Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Python pathlib support for file IO #6619

Merged
merged 3 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Loading