diff --git a/cpp/pybind/utility/eigen.cpp b/cpp/pybind/utility/eigen.cpp index f99bb05dce4..ab7d460c057 100644 --- a/cpp/pybind/utility/eigen.cpp +++ b/cpp/pybind/utility/eigen.cpp @@ -47,12 +47,12 @@ std::vector py_array_to_vectors_double( throw py::cast_error(); } std::vector eigen_vectors(array.shape(0)); - auto array_unchecked = array.mutable_unchecked<2>(); + auto array_unchecked = array.unchecked<2>(); for (auto i = 0; i < array_unchecked.shape(0); ++i) { - // The EigenVector here must be a double-typed eigen vector, since only - // open3d::Vector3dVector binds to py_array_to_vectors_double. - // Therefore, we can use the memory map directly. - eigen_vectors[i] = Eigen::Map(&array_unchecked(i, 0)); + // Vector2dVector and Vector3dVector both use double-typed Eigen + // vectors. Copy through a const map to also accept read-only arrays. + eigen_vectors[i] = + Eigen::Map(&array_unchecked(i, 0)); } return eigen_vectors; } @@ -67,9 +67,10 @@ std::vector py_array_to_vectors_int(py::array_t array) { throw py::cast_error(); } std::vector eigen_vectors(array.shape(0)); - auto array_unchecked = array.mutable_unchecked<2>(); + auto array_unchecked = array.unchecked<2>(); for (auto i = 0; i < array_unchecked.shape(0); ++i) { - eigen_vectors[i] = Eigen::Map(&array_unchecked(i, 0)); + eigen_vectors[i] = + Eigen::Map(&array_unchecked(i, 0)); } return eigen_vectors; } @@ -86,9 +87,10 @@ py_array_to_vectors_int_eigen_allocator(py::array_t array) { throw py::cast_error(); } std::vector eigen_vectors(array.shape(0)); - auto array_unchecked = array.mutable_unchecked<2>(); + auto array_unchecked = array.unchecked<2>(); for (auto i = 0; i < array_unchecked.shape(0); ++i) { - eigen_vectors[i] = Eigen::Map(&array_unchecked(i, 0)); + eigen_vectors[i] = + Eigen::Map(&array_unchecked(i, 0)); } return eigen_vectors; } @@ -105,9 +107,10 @@ py_array_to_vectors_int64_eigen_allocator(py::array_t array) { throw py::cast_error(); } std::vector eigen_vectors(array.shape(0)); - auto array_unchecked = array.mutable_unchecked<2>(); + auto array_unchecked = array.unchecked<2>(); for (auto i = 0; i < array_unchecked.shape(0); ++i) { - eigen_vectors[i] = Eigen::Map(&array_unchecked(i, 0)); + eigen_vectors[i] = + Eigen::Map(&array_unchecked(i, 0)); } return eigen_vectors; } @@ -361,7 +364,8 @@ Example usage pcd = open3d.geometry.PointCloud() np_points = np.random.rand(100, 3) - # From numpy to Open3D + # From numpy to Open3D: the input is copied and may be read-only. + np_points.setflags(write=False) pcd.points = open3d.utility.Vector3dVector(np_points) # From Open3D to numpy diff --git a/python/test/test_open3d_eigen.py b/python/test/test_open3d_eigen.py index 4d2ea8c30d0..9d6893969d7 100644 --- a/python/test/test_open3d_eigen.py +++ b/python/test/test_open3d_eigen.py @@ -156,6 +156,53 @@ def run_test(input_array): run_test(input_array) +@pytest.mark.parametrize("vector_name, width, dtype", [ + ("Vector2dVector", 2, np.float64), + ("Vector3dVector", 3, np.float64), + ("Vector2iVector", 2, np.int32), + ("Vector3iVector", 3, np.int32), + ("Vector4iVector", 4, np.int32), +]) +@pytest.mark.parametrize("writeable", [True, False], + ids=["writable", "readonly"]) +@pytest.mark.parametrize( + "layout", + ["c", "empty", "fortran", "column_slice", "reversed", "converted"]) +def test_eigen_vector_numpy_input_is_copied(vector_name, width, dtype, + writeable, layout): + input_array = np.arange(4 * width, dtype=dtype).reshape(4, width) + if layout == "empty": + input_array = input_array[:0] + elif layout == "fortran": + input_array = np.asfortranarray(input_array) + elif layout == "column_slice": + input_array = np.arange(8 * width, dtype=dtype).reshape(4, 2 * width) + input_array = input_array[:, ::2] + elif layout == "reversed": + input_array = input_array[::-1, ::-1] + elif layout == "converted": + converted_dtype = np.float32 if dtype == np.float64 else np.int64 + input_array = input_array.astype(converted_dtype) + input_array.setflags(write=writeable) + expected = input_array.copy() + + vector = getattr(o3d.utility, vector_name)(input_array) + output_array = np.asarray(vector) + + assert output_array.dtype == np.dtype(dtype) + np.testing.assert_array_equal(output_array, expected) + assert not np.shares_memory(output_array, input_array) + assert input_array.flags.writeable == writeable + np.testing.assert_array_equal(input_array, expected) + + # Open3D owns the copy even when the NumPy input is read-only. + output_array[...] = -1 + np.testing.assert_array_equal(np.asarray(vector), + np.full_like(expected, -1)) + np.testing.assert_array_equal(input_array, expected) + assert input_array.flags.writeable == writeable + + # Run with pytest -s to show output def test_benchmark(): vector_size = int(2e6)