Skip to content

Commit

Permalink
Add Python pathlib support for file IO (#6619)
Browse files Browse the repository at this point in the history
* Add pathlib support for IO functions
* Add pathlib support for visualizer and DepthNoiseSimulator
* Update changelog, add pathlib support to RGBDVideoReader
  • Loading branch information
sitic authored Feb 7, 2024
1 parent 2e47a12 commit 0a57855
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 98 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- Fix geometry picker Error when LineSet objects are presented (PR #6499)
- Fix mis-configured application .desktop link for the Open3D viewer when installing to a custom path (PR #6599)
- Fix regression in printing cuda tensor from PR #6444 🐛
- Add Python pathlib support for file IO (PR #6619)

## 0.13

Expand Down
125 changes: 63 additions & 62 deletions cpp/pybind/io/class_io.cpp

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions cpp/pybind/open3d_pybind.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
// every compilation unit.
#include "pybind/core/tensor_type_caster.h"

// Replace with <pybind11/stl/filesystem.h> when we require C++17.
#include "pybind_filesystem.h"

namespace py = pybind11;
using namespace py::literals;

Expand Down
109 changes: 109 additions & 0 deletions cpp/pybind/pybind_filesystem.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// ----------------------------------------------------------------------------
// - Open3D: www.open3d.org -
// ----------------------------------------------------------------------------
// Copyright (c) 2018-2023 www.open3d.org
// SPDX-License-Identifier: MIT
// ----------------------------------------------------------------------------

// Adapted from <pybind11/stl/filesystem.h> to support C++14.
// Original attribution:
// Copyright (c) 2021 The Pybind Development Team.
// All rights reserved. Use of this source code is governed by a
// BSD-style license.

#pragma once

#include <pybind11/cast.h>
#include <pybind11/detail/common.h>
#include <pybind11/detail/descr.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>

#include <string>

#ifdef WIN32
#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
#endif
#ifdef __APPLE__
#include <filesystem>
namespace fs = std::__fs::filesystem;
#else
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#endif

namespace pybind11 {
namespace detail {

template <typename T>
struct path_caster {
private:
static PyObject *unicode_from_fs_native(const std::string &w) {
#if !defined(PYPY_VERSION)
return PyUnicode_DecodeFSDefaultAndSize(w.c_str(), ssize_t(w.size()));
#else
// PyPy mistakenly declares the first parameter as non-const.
return PyUnicode_DecodeFSDefaultAndSize(const_cast<char *>(w.c_str()),
ssize_t(w.size()));
#endif
}

static PyObject *unicode_from_fs_native(const std::wstring &w) {
return PyUnicode_FromWideChar(w.c_str(), ssize_t(w.size()));
}

public:
static handle cast(const T &path, return_value_policy, handle) {
if (auto py_str = unicode_from_fs_native(path.native())) {
return module_::import("pathlib")
.attr("Path")(reinterpret_steal<object>(py_str))
.release();
}
return nullptr;
}

bool load(handle handle, bool) {
// PyUnicode_FSConverter and PyUnicode_FSDecoder normally take care of
// calling PyOS_FSPath themselves, but that's broken on PyPy (PyPy
// issue #3168) so we do it ourselves instead.
PyObject *buf = PyOS_FSPath(handle.ptr());
if (!buf) {
PyErr_Clear();
return false;
}
PyObject *native = nullptr;
if (std::is_same<typename T::value_type, char>::value) {
if (PyUnicode_FSConverter(buf, &native) != 0) {
if (auto *c_str = PyBytes_AsString(native)) {
// AsString returns a pointer to the internal buffer, which
// must not be free'd.
value = c_str;
}
}
} else if (std::is_same<typename T::value_type, wchar_t>::value) {
if (PyUnicode_FSDecoder(buf, &native) != 0) {
if (auto *c_str = PyUnicode_AsWideCharString(native, nullptr)) {
// AsWideCharString returns a new string that must be
// free'd.
value = c_str; // Copies the string.
PyMem_Free(c_str);
}
}
}
Py_XDECREF(native);
Py_DECREF(buf);
if (PyErr_Occurred()) {
PyErr_Clear();
return false;
}
return true;
}

PYBIND11_TYPE_CASTER(T, const_name("os.PathLike"));
};

template <>
struct type_caster<fs::path> : public path_caster<fs::path> {};

} // namespace detail
} // namespace pybind11
31 changes: 16 additions & 15 deletions cpp/pybind/t/io/class_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ void pybind_class_io(py::module &m_io) {
// open3d::t::geometry::Image
m_io.def(
"read_image",
[](const std::string &filename) {
[](const fs::path &filename) {
py::gil_scoped_release release;
geometry::Image image;
ReadImage(filename, image);
ReadImage(filename.string(), image);
return image;
},
"Function to read image from file.", "filename"_a);
Expand All @@ -83,10 +83,10 @@ void pybind_class_io(py::module &m_io) {

m_io.def(
"write_image",
[](const std::string &filename, const geometry::Image &image,
[](const fs::path &filename, const geometry::Image &image,
int quality) {
py::gil_scoped_release release;
return WriteImage(filename, image, quality);
return WriteImage(filename.string(), image, quality);
},
"Function to write Image to file.", "filename"_a, "image"_a,
"quality"_a = kOpen3DImageIODefaultQuality);
Expand All @@ -96,12 +96,12 @@ void pybind_class_io(py::module &m_io) {
// open3d::t::geometry::PointCloud
m_io.def(
"read_point_cloud",
[](const std::string &filename, const std::string &format,
[](const fs::path &filename, const std::string &format,
bool remove_nan_points, bool remove_infinite_points,
bool print_progress) {
py::gil_scoped_release release;
t::geometry::PointCloud pcd;
ReadPointCloud(filename, pcd,
ReadPointCloud(filename.string(), pcd,
{format, remove_nan_points,
remove_infinite_points, print_progress});
return pcd;
Expand All @@ -114,12 +114,12 @@ void pybind_class_io(py::module &m_io) {

m_io.def(
"write_point_cloud",
[](const std::string &filename,
[](const fs::path &filename,
const t::geometry::PointCloud &pointcloud, bool write_ascii,
bool compressed, bool print_progress) {
py::gil_scoped_release release;
return WritePointCloud(
filename, pointcloud,
filename.string(), pointcloud,
{write_ascii, compressed, print_progress});
},
"Function to write PointCloud with tensor attributes to file.",
Expand All @@ -131,14 +131,14 @@ void pybind_class_io(py::module &m_io) {
// open3d::geometry::TriangleMesh
m_io.def(
"read_triangle_mesh",
[](const std::string &filename, bool enable_post_processing,
[](const fs::path &filename, bool enable_post_processing,
bool print_progress) {
py::gil_scoped_release release;
t::geometry::TriangleMesh mesh;
open3d::io::ReadTriangleMeshOptions opt;
opt.enable_post_processing = enable_post_processing;
opt.print_progress = print_progress;
ReadTriangleMesh(filename, mesh, opt);
ReadTriangleMesh(filename.string(), mesh, opt);
return mesh;
},
"Function to read TriangleMesh from file", "filename"_a,
Expand Down Expand Up @@ -178,13 +178,12 @@ The following example reads a triangle mesh with the .ply extension::

m_io.def(
"write_triangle_mesh",
[](const std::string &filename,
const t::geometry::TriangleMesh &mesh, bool write_ascii,
bool compressed, bool write_vertex_normals,
[](const fs::path &filename, const t::geometry::TriangleMesh &mesh,
bool write_ascii, bool compressed, bool write_vertex_normals,
bool write_vertex_colors, bool write_triangle_uvs,
bool print_progress) {
py::gil_scoped_release release;
return WriteTriangleMesh(filename, mesh, write_ascii,
return WriteTriangleMesh(filename.string(), mesh, write_ascii,
compressed, write_vertex_normals,
write_vertex_colors,
write_triangle_uvs, print_progress);
Expand Down Expand Up @@ -222,7 +221,9 @@ Example::
# Save noisy depth image (uint16)
o3d.t.io.write_image("noisy_depth.png", im_dst)
)");
depth_noise_simulator.def(py::init<const std::string &>(),
depth_noise_simulator.def(py::init([](const fs::path &fielname) {
return DepthNoiseSimulator(fielname.string());
}),
"noise_model_path"_a);
depth_noise_simulator.def("simulate", &DepthNoiseSimulator::Simulate,
"im_src"_a, "depth_scale"_a = 1000.0f,
Expand Down
8 changes: 6 additions & 2 deletions cpp/pybind/t/io/sensor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,12 @@ void pybind_sensor(py::module &m) {
std::unique_ptr<RGBDVideoReader>>
rgbd_video_reader(m, "RGBDVideoReader", "RGBD Video file reader.");
rgbd_video_reader.def(py::init<>())
.def_static("create", &RGBDVideoReader::Create, "filename"_a,
"Create RGBD video reader based on filename")
.def_static(
"create",
[](const fs::path &filename) {
return RGBDVideoReader::Create(filename.string());
},
"filename"_a, "Create RGBD video reader based on filename")
.def("save_frames", &RGBDVideoReader::SaveFrames, "frame_path"_a,
"start_time_us"_a = 0, "end_time_us"_a = UINT64_MAX,
"Save synchronized and aligned individual frames to "
Expand Down
8 changes: 4 additions & 4 deletions cpp/pybind/visualization/renderoption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ void pybind_renderoption(py::module &m) {
})
.def(
"load_from_json",
[](RenderOption &ro, const std::string &filename) {
io::ReadIJsonConvertible(filename, ro);
[](RenderOption &ro, const fs::path &filename) {
io::ReadIJsonConvertible(filename.string(), ro);
},
"Function to load RenderOption from a JSON "
"file.",
"filename"_a)
.def(
"save_to_json",
[](RenderOption &ro, const std::string &filename) {
io::WriteIJsonConvertible(filename, ro);
[](RenderOption &ro, const fs::path &filename) {
io::WriteIJsonConvertible(filename.string(), ro);
},
"Function to save RenderOption to a JSON "
"file.",
Expand Down
8 changes: 4 additions & 4 deletions cpp/pybind/visualization/utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ void pybind_visualization_utility_methods(py::module &m) {
[](const std::vector<std::shared_ptr<const geometry::Geometry>>
&geometry_ptrs,
const std::string &window_name, int width, int height, int left,
int top, const std::string &json_filename) {
int top, const fs::path &json_filename) {
std::string current_dir =
utility::filesystem::GetWorkingDirectory();
DrawGeometriesWithCustomAnimation(geometry_ptrs, window_name,
width, height, left, top,
json_filename);
json_filename.string());
utility::filesystem::ChangeWorkingDirectory(current_dir);
},
"Function to draw a list of geometry::Geometry objects with a GUI "
Expand Down Expand Up @@ -251,9 +251,9 @@ void pybind_visualization_utility_methods(py::module &m) {

m.def(
"read_selection_polygon_volume",
[](const std::string &filename) {
[](const fs::path &filename) {
SelectionPolygonVolume vol;
io::ReadIJsonConvertible(filename, vol);
io::ReadIJsonConvertible(filename.string(), vol);
return vol;
},
"Function to read SelectionPolygonVolume from file", "filename"_a);
Expand Down
39 changes: 29 additions & 10 deletions cpp/pybind/visualization/visualizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,39 @@ void pybind_visualizer(py::module &m) {
&Visualizer::CaptureScreenFloatBuffer,
"Function to capture screen and store RGB in a float buffer",
"do_render"_a = false)
.def("capture_screen_image", &Visualizer::CaptureScreenImage,
"Function to capture and save a screen image", "filename"_a,
"do_render"_a = false)
.def(
"capture_screen_image",
[](Visualizer &self, const fs::path &filename,
bool do_render) {
return self.CaptureScreenImage(filename.string(),
do_render);
},
"Function to capture and save a screen image", "filename"_a,
"do_render"_a = false)
.def("capture_depth_float_buffer",
&Visualizer::CaptureDepthFloatBuffer,
"Function to capture depth in a float buffer",
"do_render"_a = false)
.def("capture_depth_image", &Visualizer::CaptureDepthImage,
"Function to capture and save a depth image", "filename"_a,
"do_render"_a = false, "depth_scale"_a = 1000.0)
.def("capture_depth_point_cloud",
&Visualizer::CaptureDepthPointCloud,
"Function to capture and save local point cloud", "filename"_a,
"do_render"_a = false, "convert_to_world_coordinate"_a = false)
.def(
"capture_depth_image",
[](Visualizer &self, const fs::path &filename,
bool do_render, double depth_scale) {
self.CaptureDepthImage(filename.string(), do_render,
depth_scale);
},
"Function to capture and save a depth image", "filename"_a,
"do_render"_a = false, "depth_scale"_a = 1000.0)
.def(
"capture_depth_point_cloud",
[](Visualizer &self, const fs::path &filename,
bool do_render, bool convert_to_world_coordinate) {
self.CaptureDepthPointCloud(
filename.string(), do_render,
convert_to_world_coordinate);
},
"Function to capture and save local point cloud",
"filename"_a, "do_render"_a = false,
"convert_to_world_coordinate"_a = false)
.def("get_window_name", &Visualizer::GetWindowName)
.def("get_view_status", &Visualizer::GetViewStatus,
"Get the current view status as a json string of "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def mse(image, ref_img):


def load_input_mesh(model_path, tex_dim):
mesh = o3d.t.io.read_triangle_mesh(str(model_path))
mesh = o3d.t.io.read_triangle_mesh(model_path)
mesh.material.set_default_properties()
mesh.material.material_name = 'defaultLit' # note: ignored by Mitsuba, just used to visualize in Open3D
mesh.material.texture_maps['albedo'] = o3d.t.geometry.Image(0.5 + np.zeros(
Expand Down
21 changes: 21 additions & 0 deletions python/test/io/test_pathlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ----------------------------------------------------------------------------
# - Open3D: www.open3d.org -
# ----------------------------------------------------------------------------
# Copyright (c) 2018-2023 www.open3d.org
# SPDX-License-Identifier: MIT
# ----------------------------------------------------------------------------

from pathlib import Path

import open3d as o3d


def test_pathlib_support():
pcd_pointcloud = o3d.data.PCDPointCloud()
assert isinstance(pcd_pointcloud.path, str)

pcd = o3d.io.read_point_cloud(pcd_pointcloud.path)
assert pcd.has_points()

pcd = o3d.io.read_point_cloud(Path(pcd_pointcloud.path))
assert pcd.has_points()

0 comments on commit 0a57855

Please sign in to comment.