From e920e22604f6d294c6087ba81ee871045e4c7571 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Tue, 31 Dec 2024 23:30:37 -0800 Subject: [PATCH] Create albedo texture for a model from calibrated images (#6806) Create an albedo for the triangle mesh using calibrated images. The triangle mesh must have texture coordinates (texture_uvs triangle attribute). This works by back projecting the images onto the texture surface. Overlapping images are blended together in the resulting albedo. For best results, use images captured with exposure and white balance lock to reduce the chance of seams in the output texture. --- cpp/open3d/core/TensorKey.h | 4 +- cpp/open3d/io/ImageIO.cpp | 3 +- cpp/open3d/t/geometry/Image.cpp | 6 + cpp/open3d/t/geometry/RaycastingScene.cpp | 3 +- cpp/open3d/t/geometry/RaycastingScene.h | 2 +- cpp/open3d/t/geometry/TriangleMesh.cpp | 272 +++++++++++++++++- cpp/open3d/t/geometry/TriangleMesh.h | 31 +- cpp/open3d/t/geometry/Utility.h | 2 +- cpp/open3d/t/geometry/VoxelBlockGrid.cpp | 4 +- cpp/open3d/t/geometry/kernel/IPPImage.cpp | 113 +++++++- cpp/open3d/t/geometry/kernel/IPPImage.h | 38 +-- cpp/open3d/t/io/ImageIO.cpp | 3 +- cpp/open3d/t/io/file_format/FilePNG.cpp | 4 +- cpp/open3d/visualization/gui/Application.cpp | 2 + cpp/pybind/t/geometry/trianglemesh.cpp | 31 ++ cpp/pybind/visualization/o3dvisualizer.cpp | 4 +- cpp/tests/t/geometry/TriangleMesh.cpp | 55 ++++ docs/tutorial/data/index.rst | 10 +- .../triangle_mesh_project_to_albedo.py | 203 +++++++++++++ .../python/visualization/remove_geometry.py | 3 +- python/open3d/visualization/draw.py | 6 +- 21 files changed, 742 insertions(+), 57 deletions(-) create mode 100644 examples/python/geometry/triangle_mesh_project_to_albedo.py diff --git a/cpp/open3d/core/TensorKey.h b/cpp/open3d/core/TensorKey.h index d5b89ad2010..55c49df7aed 100644 --- a/cpp/open3d/core/TensorKey.h +++ b/cpp/open3d/core/TensorKey.h @@ -82,11 +82,11 @@ class TensorKey { /// For TensorKeyMode::Slice only. int64_t GetStart() const; - /// Get stop index. Throws exception if start is None. + /// Get stop index. Throws exception if stop is None. /// For TensorKeyMode::Slice only. int64_t GetStop() const; - /// Get step index. Throws exception if start is None. + /// Get step index. Throws exception if step is None. /// For TensorKeyMode::Slice only. int64_t GetStep() const; diff --git a/cpp/open3d/io/ImageIO.cpp b/cpp/open3d/io/ImageIO.cpp index 737d2ebbdaf..dd7d3df06b7 100644 --- a/cpp/open3d/io/ImageIO.cpp +++ b/cpp/open3d/io/ImageIO.cpp @@ -81,7 +81,8 @@ bool WriteImage(const std::string &filename, auto map_itr = file_extension_to_image_write_function.find(filename_ext); if (map_itr == file_extension_to_image_write_function.end()) { utility::LogWarning( - "Write geometry::Image failed: unknown file extension."); + "Write geometry::Image failed: file extension {} unknown.", + filename_ext); return false; } return map_itr->second(filename, image, quality); diff --git a/cpp/open3d/t/geometry/Image.cpp b/cpp/open3d/t/geometry/Image.cpp index 25bd1d1951c..91430113405 100644 --- a/cpp/open3d/t/geometry/Image.cpp +++ b/cpp/open3d/t/geometry/Image.cpp @@ -156,6 +156,12 @@ Image Image::Resize(float sampling_rate, InterpType interp_type) const { if (sampling_rate == 1.0f) { return *this; } + if (GetDtype() == core::Bool) { // Resize via UInt8 + return Image(Image(data_.ReinterpretCast(core::UInt8)) + .Resize(sampling_rate, interp_type) + .AsTensor() + .ReinterpretCast(core::Bool)); + } static const dtype_channels_pairs npp_supported{ {core::UInt8, 1}, {core::UInt16, 1}, {core::Float32, 1}, diff --git a/cpp/open3d/t/geometry/RaycastingScene.cpp b/cpp/open3d/t/geometry/RaycastingScene.cpp index 436c549bd08..0d4399f65bc 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.cpp +++ b/cpp/open3d/t/geometry/RaycastingScene.cpp @@ -1313,7 +1313,7 @@ uint32_t RaycastingScene::AddTriangles(const TriangleMesh& mesh) { } std::unordered_map RaycastingScene::CastRays( - const core::Tensor& rays, const int nthreads) { + const core::Tensor& rays, const int nthreads) const { AssertTensorDtypeLastDimDeviceMinNDim(rays, "rays", 6, impl_->tensor_device_); auto shape = rays.GetShape(); @@ -1723,7 +1723,6 @@ uint32_t RaycastingScene::INVALID_ID() { return RTC_INVALID_GEOMETRY_ID; } } // namespace geometry } // namespace t } // namespace open3d - namespace fmt { template <> struct formatter { diff --git a/cpp/open3d/t/geometry/RaycastingScene.h b/cpp/open3d/t/geometry/RaycastingScene.h index bd565c4d18f..3f0ff7e3d5e 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.h +++ b/cpp/open3d/t/geometry/RaycastingScene.h @@ -72,7 +72,7 @@ class RaycastingScene { /// - \b primitive_normals A tensor with the normals of the hit /// triangles. The shape is {.., 3}. std::unordered_map CastRays( - const core::Tensor &rays, const int nthreads = 0); + const core::Tensor &rays, const int nthreads = 0) const; /// \brief Checks if the rays have any intersection with the scene. /// \param rays A tensor with >=2 dims, shape {.., 6}, and Dtype Float32 diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 05e8cc7b66f..17461b7214c 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -7,6 +7,8 @@ #include "open3d/t/geometry/TriangleMesh.h" +#include +#include #include #include #include @@ -16,26 +18,42 @@ #include #include +#include +#include +#include +#include +#include #include +#include #include +#include +#include #include "open3d/core/CUDAUtils.h" #include "open3d/core/Device.h" #include "open3d/core/Dtype.h" #include "open3d/core/EigenConverter.h" +#include "open3d/core/ParallelFor.h" #include "open3d/core/ShapeUtil.h" #include "open3d/core/Tensor.h" #include "open3d/core/TensorCheck.h" +#include "open3d/core/TensorKey.h" +#include "open3d/core/linalg/AddMM.h" +#include "open3d/core/linalg/Matmul.h" +#include "open3d/core/nns/NearestNeighborSearch.h" #include "open3d/t/geometry/LineSet.h" #include "open3d/t/geometry/PointCloud.h" #include "open3d/t/geometry/RaycastingScene.h" #include "open3d/t/geometry/VtkUtils.h" +#include "open3d/t/geometry/kernel/IPPImage.h" #include "open3d/t/geometry/kernel/Metrics.h" #include "open3d/t/geometry/kernel/PCAPartition.h" #include "open3d/t/geometry/kernel/PointCloud.h" #include "open3d/t/geometry/kernel/Transform.h" #include "open3d/t/geometry/kernel/TriangleMesh.h" #include "open3d/t/geometry/kernel/UVUnwrapping.h" +#include "open3d/t/io/ImageIO.h" +#include "open3d/t/io/NumpyIO.h" #include "open3d/utility/ParallelScan.h" namespace open3d { @@ -1022,7 +1040,7 @@ TriangleMesh::BakeTriangleAttrTextures( } core::Tensor tensor = triangle_attr_.at(attr).To(core::Device()).Contiguous(); - DISPATCH_DTYPE_TO_TEMPLATE(tensor.GetDtype(), [&]() { + DISPATCH_DTYPE_TO_TEMPLATE_WITH_BOOL(tensor.GetDtype(), [&]() { core::Tensor tex; if (GetTriangleIndices().GetDtype() == core::Int32) { tex = BakeAttribute( @@ -1199,7 +1217,8 @@ static bool IsNegative(T val) { return false; } -TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices) const { +TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices, + bool copy_attributes /*=true*/) const { core::AssertTensorShape(indices, {indices.GetLength()}); if (indices.NumElements() == 0) { return {}; @@ -1299,7 +1318,8 @@ TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices) const { if (tris_cpu.NumElements() > 0) { // To() needs non-empty tensor result.SetTriangleIndices(tris_cpu.To(GetDevice())); } - CopyAttributesByMasks(result, *this, vertex_mask, tri_mask); + if (copy_attributes) + CopyAttributesByMasks(result, *this, vertex_mask, tri_mask); return result; } @@ -1354,16 +1374,260 @@ TriangleMesh TriangleMesh::RemoveUnreferencedVertices() { utility::LogDebug( "[RemoveUnreferencedVertices] {:d} vertices have been removed.", - (int)(num_verts_old - GetVertexPositions().GetLength())); + static_cast(num_verts_old - GetVertexPositions().GetLength())); return *this; } +namespace { + +core::Tensor Project(const core::Tensor &t_xyz, // contiguous {...,3} + const core::Tensor &t_intrinsic_matrix, // {3,3} + const core::Tensor &t_extrinsic_matrix, // {4,4} + core::Tensor &t_xy) { // contiguous {...,2} + auto xy_shape = t_xyz.GetShape(); + auto dt = t_xyz.GetDtype(); + auto t_K = t_intrinsic_matrix.To(dt).Contiguous(), + t_T = t_extrinsic_matrix.To(dt).Contiguous(); + xy_shape[t_xyz.NumDims() - 1] = 2; + if (t_xy.GetDtype() != dt || t_xy.GetShape() != xy_shape) { + t_xy = core::Tensor(xy_shape, dt); + } + DISPATCH_FLOAT_DTYPE_TO_TEMPLATE(dt, [&]() { + // Eigen is column major + Eigen::Map> xy(t_xy.GetDataPtr(), 2, + t_xy.NumElements() / 2); + Eigen::Map> xyz( + t_xyz.GetDataPtr(), 3, t_xyz.NumElements() / 3); + Eigen::Map> KT( + t_K.GetDataPtr()); + Eigen::Map> TT( + t_T.GetDataPtr()); + + auto K = KT.transpose(); + auto T = TT.transpose(); + auto R = T.topLeftCorner<3, 3>(); + auto t = T.topRightCorner<3, 1>(); + auto pxyz = (K * ((R * xyz).colwise() + t)).array(); + xy = pxyz.topRows<2>().rowwise() / pxyz.bottomRows<1>(); + }); + return t_xy; // contiguous {...,2} +} + +/// Estimate minimum sqr distance from a set of points to a set of cameras. +float get_min_cam_sqrdistance( + const core::Tensor &positions, + const std::vector &extrinsic_matrices) { + const size_t MAXPTS = 10000; + core::Tensor cam_loc({int64_t(extrinsic_matrices.size()), 3}, + core::Float32); + for (size_t k = 0; k < extrinsic_matrices.size(); ++k) { + const core::Tensor RT = extrinsic_matrices[k].Slice(0, 0, 3); + cam_loc[k] = + -RT.Slice(1, 0, 3).T().Matmul(RT.Slice(1, 3, 4)).Reshape({-1}); + } + size_t npts = positions.GetShape(0); + const core::Tensor pos_sample = + npts > MAXPTS ? positions.Slice(0, 0, -1, npts / MAXPTS) + : positions; + auto nns = core::nns::NearestNeighborSearch(pos_sample); + nns.KnnIndex(); + float min_sqrdistance = nns.KnnSearch(cam_loc, 1) + .second.Min({0, 1}) + .To(core::Device(), core::Float32) + .Item(); + return min_sqrdistance; +} + +} // namespace + +Image TriangleMesh::ProjectImagesToAlbedo( + const std::vector &images, + const std::vector &intrinsic_matrices, + const std::vector &extrinsic_matrices, + int tex_size /*=1024*/, + bool update_material /*=true*/) { + using core::None; + using tk = core::TensorKey; + constexpr float EPS = 1e-6; + if (!HasTriangleAttr("texture_uvs")) { + utility::LogError( + "TriangleMesh does not contain 'texture_uvs'. Please compute " + "it with ComputeUVAtlas() first."); + } + core::Tensor texture_uvs = + GetTriangleAttr("texture_uvs").To(core::Device()).Contiguous(); + core::AssertTensorShape(texture_uvs, {core::None, 3, 2}); + core::AssertTensorDtype(texture_uvs, {core::Float32}); + + if (images.size() != extrinsic_matrices.size() || + images.size() != intrinsic_matrices.size()) { + utility::LogError( + "Received {} images, but {} extrinsic matrices and {} " + "intrinsic matrices.", + images.size(), extrinsic_matrices.size(), + intrinsic_matrices.size()); + } + + // softmax_shift is used to prevent overflow in the softmax function. + // softmax_shift is set so that max value of weighting function is exp(64), + // well within float range. (exp(89.f) is inf) + float min_sqr_distance = + get_min_cam_sqrdistance(GetVertexPositions(), extrinsic_matrices); + float softmax_shift = 10.f, softmax_scale = 20 * min_sqr_distance; + // (u,v) -> (x,y,z) : {tex_size, tex_size, 3} + core::Tensor position_map = BakeVertexAttrTextures( + tex_size, {"positions"}, 1, 0, false)["positions"]; + core::Tensor albedo = + core::Tensor::Zeros({tex_size, tex_size, 4}, core::Float32); + albedo.Slice(2, 3, 4).Fill(EPS); // regularize + std::mutex albedo_mutex; + + RaycastingScene rcs; + rcs.AddTriangles(*this); + + // setup working data for each task. + size_t max_workers = tbb::this_task_arena::max_concurrency(); + // Tensor copy ctor does shallow copies - OK for empty tensors. + std::vector this_albedo(max_workers, + core::Tensor({}, core::Float32)), + weighted_image(max_workers, core::Tensor({}, core::Float32)), + uv2xy(max_workers, core::Tensor({}, core::Float32)), + uvrays(max_workers, core::Tensor({}, core::Float32)); + + auto project_one_image = [&](size_t i, tbb::feeder &feeder) { + size_t widx = tbb::this_task_arena::current_thread_index(); + // initialize task variables + if (!this_albedo[widx].GetShape().IsCompatible( + {tex_size, tex_size, 4})) { + this_albedo[widx] = + core::Tensor::Empty({tex_size, tex_size, 4}, core::Float32); + uvrays[widx] = + core::Tensor::Empty({tex_size, tex_size, 6}, core::Float32); + } + auto width = images[i].GetCols(), height = images[i].GetRows(); + if (!weighted_image[widx].GetShape().IsCompatible({height, width, 4})) { + weighted_image[widx] = + core::Tensor({height, width, 4}, core::Float32); + } + core::AssertTensorShape(intrinsic_matrices[i], {3, 3}); + core::AssertTensorShape(extrinsic_matrices[i], {4, 4}); + + // A. Get image space weight matrix, as inverse of pixel + // footprint on the mesh. + auto rays = RaycastingScene::CreateRaysPinhole( + intrinsic_matrices[i], extrinsic_matrices[i], width, height); + core::Tensor cam_loc = + rays.GetItem({tk::Index(0), tk::Index(0), tk::Slice(0, 3, 1)}); + + // A nested parallel_for's threads must be isolated from the threads + // running this paralel_for, else we get BAD ACCESS errors. + auto result = tbb::this_task_arena::isolate( + [&rays, &rcs]() { return rcs.CastRays(rays); }); + // Eigen is column-major order + Eigen::Map normals_e( + result["primitive_normals"].GetDataPtr(), 3, + width * height); + Eigen::Map rays_e(rays.GetDataPtr(), 6, + width * height); + Eigen::Map t_hit(result["t_hit"].GetDataPtr(), + 1, width * height); + auto depth = t_hit * rays_e.bottomRows<3>().colwise().norm().array(); + // removing this eval() increase runtime a lot (?) + auto rays_dir = rays_e.bottomRows<3>().colwise().normalized().eval(); + auto pixel_foreshortening = (normals_e * rays_dir) + .colwise() + .sum() + .abs(); // ignore face orientation + // fix for bad normals + auto inv_footprint = + pixel_foreshortening.isNaN().select(0, pixel_foreshortening) / + (depth * depth); + utility::LogDebug( + "[ProjectImagesToAlbedo] Image {}, weight (inv_footprint) " + "range: {}-{}", + i, inv_footprint.minCoeff(), inv_footprint.maxCoeff()); + weighted_image[widx].Slice(2, 0, 3) = + images[i].To(core::Float32).AsTensor(); // range: [0,1] + Eigen::Map weighted_image_e( + weighted_image[widx].GetDataPtr(), 4, width * height); + weighted_image_e.bottomRows<1>() = inv_footprint; + + // B. Get texture space (u,v) -> (x,y) map and valid domain in + // uv space. + uvrays[widx].GetItem({tk::Slice(0, None, 1), tk::Slice(0, None, 1), + tk::Slice(0, 3, 1)}) = cam_loc; + uvrays[widx].GetItem({tk::Slice(0, None, 1), tk::Slice(0, None, 1), + tk::Slice(3, 6, 1)}) = position_map - cam_loc; + // A nested parallel_for's threads must be isolated from the threads + // running this paralel_for, else we get BAD ACCESS errors. + result = tbb::this_task_arena::isolate( + [&rcs, &uvrays, widx]() { return rcs.CastRays(uvrays[widx]); }); + auto &t_hit_uv = result["t_hit"]; + + Project(position_map, intrinsic_matrices[i], extrinsic_matrices[i], + uv2xy[widx]); // {ts, ts, 2} + // Disable self-occluded points + for (float *p_uv2xy = uv2xy[widx].GetDataPtr(), + *p_t_hit = t_hit_uv.GetDataPtr(); + p_uv2xy < + uv2xy[widx].GetDataPtr() + uv2xy[widx].NumElements(); + p_uv2xy += 2, ++p_t_hit) { + if (*p_t_hit < 1 - EPS) *p_uv2xy = *(p_uv2xy + 1) = -1.f; + } + core::Tensor uv2xy2 = + uv2xy[widx].Permute({2, 0, 1}).Contiguous(); // {2, ts, ts} + + // C. Interpolate weighted image to weighted texture + // albedo[u,v] = image[ i[u,v], j[u,v] ] + this_albedo[widx].Fill(0.f); + ipp::Remap(weighted_image[widx], /*{height, width, 4} f32*/ + uv2xy2[0], /* {texsz, texsz} f32*/ + uv2xy2[1], /* {texsz, texsz} f32*/ + this_albedo[widx], /*{texsz, texsz, 4} f32*/ + t::geometry::Image::InterpType::Linear); + // Weights can become negative with higher order interpolation + + std::unique_lock albedo_lock{albedo_mutex}; + // ^^^ released when lambda returns. + for (auto p_albedo = albedo.GetDataPtr(), + p_this_albedo = this_albedo[widx].GetDataPtr(); + p_albedo < albedo.GetDataPtr() + albedo.NumElements(); + p_albedo += 4, p_this_albedo += 4) { + float softmax_weight = + exp(softmax_scale * p_this_albedo[3] - softmax_shift); + for (auto k = 0; k < 3; ++k) + p_albedo[k] += p_this_albedo[k] * softmax_weight; + p_albedo[3] += softmax_weight; + } + }; + + std::vector range(images.size(), 0); + std::iota(range.begin(), range.end(), 0); + tbb::parallel_for_each(range, project_one_image); + albedo.Slice(2, 0, 3) /= albedo.Slice(2, 3, 4); + + // Image::To uses saturate_cast + Image albedo_texture = + Image(albedo.Slice(2, 0, 3).Contiguous()) + .To(core::UInt8, /*copy=*/true, /*scale=*/255.f); + if (update_material) { + if (!HasMaterial()) { + SetMaterial(visualization::rendering::Material()); + GetMaterial().SetDefaultProperties(); // defaultUnlit + } + GetMaterial().SetAlbedoMap(albedo_texture); + } + return albedo_texture; +} + +namespace { template ::value && !std::is_same::value, T>::type * = nullptr> using Edge = std::tuple; +} /// brief Helper function to get an edge with ordered vertex indices. template diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 2b324751004..b1bd980f8b6 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include "open3d/core/Tensor.h" @@ -985,15 +986,43 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// the mesh or has a negative value, it is ignored. /// \param indices An integer list of indices. Duplicates are /// allowed, but ignored. Signed and unsigned integral types are allowed. + /// \param copy_attributes Indicates if vertex attributes (other than + /// positions) and triangle attributes (other than indices) should be copied + /// to the returned mesh. /// \return A new mesh with the selected vertices and faces built /// from the selected vertices. If the original mesh is empty, return /// an empty mesh. - TriangleMesh SelectByIndex(const core::Tensor &indices) const; + TriangleMesh SelectByIndex(const core::Tensor &indices, + bool copy_attributes = true) const; /// Removes unreferenced vertices from the mesh. /// \return The reference to itself. TriangleMesh RemoveUnreferencedVertices(); + /// Create an albedo for the triangle mesh using calibrated images. The + /// triangle mesh must have texture coordinates ("texture_uvs" triangle + /// attribute). This works by back projecting the images onto the texture + /// surface. Overlapping images are blended together in the resulting + /// albedo. For best results, use images captured with exposure and white + /// balance lock to reduce the chance of seams in the output texture. + /// + /// \param images vector of images. + /// \param intrinsic_matrices vector of {3,3} intrinsic matrices describing + /// the pinhole camera. + /// \param extrinsic_matrices vector of {4,4} extrinsic matrices describing + /// the position and orientation of the camera. + /// \param tex_size Output albedo texture size. This is a square image, so + /// only one side is needed. + /// \param update_material Whether to update the material of the triangle + /// mesh, possibly overwriting an existing albedo texture. + /// \return Image with albedo texture + Image ProjectImagesToAlbedo( + const std::vector &images, + const std::vector &intrinsic_matrices, + const std::vector &extrinsic_matrices, + int tex_size = 1024, + bool update_material = true); + /// Removes all non-manifold edges, by successively deleting triangles /// with the smallest surface area adjacent to the /// non-manifold edge until the number of adjacent triangles to the edge is diff --git a/cpp/open3d/t/geometry/Utility.h b/cpp/open3d/t/geometry/Utility.h index c96521953ae..90ddbd1081e 100644 --- a/cpp/open3d/t/geometry/Utility.h +++ b/cpp/open3d/t/geometry/Utility.h @@ -66,7 +66,7 @@ inline void CheckExtrinsicTensor(const core::Tensor& extrinsic) { } } -inline void CheckBlockCoorinates(const core::Tensor& block_coords) { +inline void CheckBlockCoordinates(const core::Tensor& block_coords) { if (block_coords.GetDtype() != core::Dtype::Int32) { utility::LogError("Unsupported block coordinate dtype {}", block_coords.GetDtype().ToString()); diff --git a/cpp/open3d/t/geometry/VoxelBlockGrid.cpp b/cpp/open3d/t/geometry/VoxelBlockGrid.cpp index 063e271658e..4e1725acec3 100644 --- a/cpp/open3d/t/geometry/VoxelBlockGrid.cpp +++ b/cpp/open3d/t/geometry/VoxelBlockGrid.cpp @@ -301,7 +301,7 @@ void VoxelBlockGrid::Integrate(const core::Tensor &block_coords, AssertInitialized(); bool integrate_color = color.AsTensor().NumElements() > 0; - CheckBlockCoorinates(block_coords); + CheckBlockCoordinates(block_coords); CheckDepthTensor(depth.AsTensor()); if (integrate_color) { CheckColorTensor(color.AsTensor()); @@ -338,7 +338,7 @@ TensorMap VoxelBlockGrid::RayCast(const core::Tensor &block_coords, float trunc_voxel_multiplier, int range_map_down_factor) { AssertInitialized(); - CheckBlockCoorinates(block_coords); + CheckBlockCoordinates(block_coords); CheckIntrinsicTensor(intrinsic); CheckExtrinsicTensor(extrinsic); diff --git a/cpp/open3d/t/geometry/kernel/IPPImage.cpp b/cpp/open3d/t/geometry/kernel/IPPImage.cpp index ba32d1af317..1535673db30 100644 --- a/cpp/open3d/t/geometry/kernel/IPPImage.cpp +++ b/cpp/open3d/t/geometry/kernel/IPPImage.cpp @@ -7,18 +7,26 @@ #include "open3d/t/geometry/kernel/IPPImage.h" -#ifdef APPLE // macOS IPP <=v2021.9 uses old directory layout +#include + +#if IPP_VERSION_INT < \ + 20211000 // macOS IPP v2021.9.11 uses old directory layout +#include + #include #include #include #include #else // Linux and Windows IPP >=v2021.10 uses new directory layout +#include + #include #include #include #include #endif +#include "open3d/core/Dispatch.h" #include "open3d/core/Dtype.h" #include "open3d/core/ParallelFor.h" #include "open3d/core/ShapeUtil.h" @@ -83,9 +91,9 @@ void RGBToGray(const core::Tensor &src_im, core::Tensor &dst_im) { } } -void Resize(const open3d::core::Tensor &src_im, - open3d::core::Tensor &dst_im, - t::geometry::Image::InterpType interp_type) { +void Resize(const core::Tensor &src_im, + core::Tensor &dst_im, + Image::InterpType interp_type) { auto dtype = src_im.GetDtype(); // Create IPP wrappers for all Open3D tensors const ::ipp::IwiImage ipp_src_im( @@ -99,14 +107,13 @@ void Resize(const open3d::core::Tensor &src_im, 0 /* border buffer size */, dst_im.GetDataPtr(), dst_im.GetStride(0) * dtype.ByteSize()); - static const std::unordered_map + static const std::unordered_map type_dict = { - {t::geometry::Image::InterpType::Nearest, ippNearest}, - {t::geometry::Image::InterpType::Linear, ippLinear}, - {t::geometry::Image::InterpType::Cubic, ippCubic}, - {t::geometry::Image::InterpType::Lanczos, ippLanczos}, - {t::geometry::Image::InterpType::Super, ippSuper}, + {Image::InterpType::Nearest, ippNearest}, + {Image::InterpType::Linear, ippLinear}, + {Image::InterpType::Cubic, ippCubic}, + {Image::InterpType::Lanczos, ippLanczos}, + {Image::InterpType::Super, ippSuper}, }; auto it = type_dict.find(interp_type); @@ -160,9 +167,9 @@ void Dilate(const core::Tensor &src_im, core::Tensor &dst_im, int kernel_size) { } } -void Filter(const open3d::core::Tensor &src_im, - open3d::core::Tensor &dst_im, - const open3d::core::Tensor &kernel) { +void Filter(const core::Tensor &src_im, + core::Tensor &dst_im, + const core::Tensor &kernel) { // Supported device and datatype checking happens in calling code and will // result in an exception if there are errors. auto dtype = src_im.GetDtype(); @@ -298,7 +305,85 @@ void FilterSobel(const core::Tensor &src_im, utility::LogError("IPP-IW error {}: {}", e.m_status, e.m_string); } } + +// Plain IPP functions + +void Remap(const core::Tensor &src_im, /*{Ws, Hs, C}*/ + const core::Tensor &dst2src_xmap, /*{Wd, Hd}, float*/ + const core::Tensor &dst2src_ymap, /*{Wd, Hd}, float*/ + core::Tensor &dst_im, /*{Wd, Hd, C}*/ + Image::InterpType interp_type) { + auto dtype = src_im.GetDtype(); + if (dtype != dst_im.GetDtype()) { + utility::LogError( + "Source ({}) and destination ({}) image dtypes are different!", + dtype.ToString(), dst_im.GetDtype().ToString()); + } + if (dst2src_xmap.GetDtype() != core::Float32) { + utility::LogError("dst2src_xmap dtype ({}) must be Float32.", + dst2src_xmap.GetDtype().ToString()); + } + if (dst2src_ymap.GetDtype() != core::Float32) { + utility::LogError("dst2src_ymap dtype ({}) must be Float32.", + dst2src_ymap.GetDtype().ToString()); + } + + static const std::unordered_map interp_dict = { + {Image::InterpType::Nearest, IPPI_INTER_NN}, + {Image::InterpType::Linear, IPPI_INTER_LINEAR}, + {Image::InterpType::Cubic, IPPI_INTER_CUBIC}, + {Image::InterpType::Lanczos, IPPI_INTER_LANCZOS}, + /* {Image::InterpType::Cubic2p_CatmullRom, */ + /* IPPI_INTER_CUBIC2P_CATMULLROM}, */ + }; + + auto interp_it = interp_dict.find(interp_type); + if (interp_it == interp_dict.end()) { + utility::LogError("Unsupported interp type {}", + static_cast(interp_type)); + } + + IppiSize src_size{static_cast(src_im.GetShape(1)), + static_cast(src_im.GetShape(0))}, + dst_roi_size{static_cast(dst_im.GetShape(1)), + static_cast(dst_im.GetShape(0))}; + IppiRect src_roi{0, 0, static_cast(src_im.GetShape(1)), + static_cast(src_im.GetShape(0))}; + IppStatus sts = ippStsNoErr; + + int src_step = src_im.GetDtype().ByteSize() * src_im.GetStride(0); + int dst_step = dst_im.GetDtype().ByteSize() * dst_im.GetStride(0); + int xmap_step = + dst2src_xmap.GetDtype().ByteSize() * dst2src_xmap.GetStride(0); + int ymap_step = + dst2src_ymap.GetDtype().ByteSize() * dst2src_ymap.GetStride(0); + if (src_im.GetDtype() == core::Float32 && src_im.GetShape(2) == 4) { + /* IPPAPI(IppStatus, ippiRemap_32f_C4R, (const Ipp32f* pSrc, IppiSize + * srcSize, */ + /* int srcStep, IppiRect srcROI, const Ipp32f* pxMap, int xMapStep, + */ + /* const Ipp32f* pyMap, int yMapStep, Ipp32f* pDst, int dstStep, */ + /* IppiSize dstRoiSize, int interpolation)) */ + const auto p_src_im = src_im.GetDataPtr(); + auto p_dst_im = dst_im.GetDataPtr(); + const auto p_dst2src_xmap = dst2src_xmap.GetDataPtr(); + const auto p_dst2src_ymap = dst2src_ymap.GetDataPtr(); + sts = ippiRemap_32f_C4R(p_src_im, src_size, src_step, src_roi, + p_dst2src_xmap, xmap_step, p_dst2src_ymap, + ymap_step, p_dst_im, dst_step, dst_roi_size, + interp_it->second); + } else { + utility::LogError( + "Remap not implemented for dtype ({}) and channels ({}).", + src_im.GetDtype().ToString(), src_im.GetShape(2)); + } + if (sts != ippStsNoErr) { + // See comments in icv/include/ippicv_types.h for meaning + utility::LogError("IPP remap error {}", ippGetStatusString(sts)); + } +} } // namespace ipp + } // namespace geometry } // namespace t } // namespace open3d diff --git a/cpp/open3d/t/geometry/kernel/IPPImage.h b/cpp/open3d/t/geometry/kernel/IPPImage.h index 0903acffa14..0229e9c903b 100644 --- a/cpp/open3d/t/geometry/kernel/IPPImage.h +++ b/cpp/open3d/t/geometry/kernel/IPPImage.h @@ -7,8 +7,9 @@ #pragma once #ifdef WITH_IPP +// Not available for Remap // Auto-enable multi-threaded implementations -#define IPP_ENABLED_THREADING_LAYER_REDEFINITIONS 1 +// #define IPP_ENABLED_THREADING_LAYER_REDEFINITIONS 1 #define IPP_CALL(ipp_function, ...) ipp_function(__VA_ARGS__); #if IPP_VERSION_INT < \ @@ -54,33 +55,38 @@ void To(const core::Tensor &src_im, void RGBToGray(const core::Tensor &src_im, core::Tensor &dst_im); -void Dilate(const open3d::core::Tensor &srcim, - open3d::core::Tensor &dstim, - int kernel_size); +void Dilate(const core::Tensor &srcim, core::Tensor &dstim, int kernel_size); -void Resize(const open3d::core::Tensor &srcim, - open3d::core::Tensor &dstim, +void Resize(const core::Tensor &srcim, + core::Tensor &dstim, t::geometry::Image::InterpType interp_type); -void Filter(const open3d::core::Tensor &srcim, - open3d::core::Tensor &dstim, - const open3d::core::Tensor &kernel); +void Filter(const core::Tensor &srcim, + core::Tensor &dstim, + const core::Tensor &kernel); -void FilterBilateral(const open3d::core::Tensor &srcim, - open3d::core::Tensor &dstim, +void FilterBilateral(const core::Tensor &srcim, + core::Tensor &dstim, int kernel_size, float value_sigma, float distance_sigma); -void FilterGaussian(const open3d::core::Tensor &srcim, - open3d::core::Tensor &dstim, +void FilterGaussian(const core::Tensor &srcim, + core::Tensor &dstim, int kernel_size, float sigma); -void FilterSobel(const open3d::core::Tensor &srcim, - open3d::core::Tensor &dstim_dx, - open3d::core::Tensor &dstim_dy, +void FilterSobel(const core::Tensor &srcim, + core::Tensor &dstim_dx, + core::Tensor &dstim_dy, int kernel_size); + +void Remap(const core::Tensor &src_im, /*{Ws, Hs, C}*/ + const core::Tensor &dst2src_xmap, /*{Wd, Hd}, float*/ + const core::Tensor &dst2src_ymap, /*{Wd, Hd, 2}, float*/ + core::Tensor &dst_im, /*{Wd, Hd, 2}*/ + Image::InterpType interp_type); + } // namespace ipp } // namespace geometry } // namespace t diff --git a/cpp/open3d/t/io/ImageIO.cpp b/cpp/open3d/t/io/ImageIO.cpp index 97941c517ef..b44d634fee9 100644 --- a/cpp/open3d/t/io/ImageIO.cpp +++ b/cpp/open3d/t/io/ImageIO.cpp @@ -88,7 +88,8 @@ bool WriteImage(const std::string &filename, auto map_itr = file_extension_to_image_write_function.find(filename_ext); if (map_itr == file_extension_to_image_write_function.end()) { utility::LogWarning( - "Write geometry::Image failed: unknown file extension."); + "Write geometry::Image failed: file extension {} unknown.", + filename_ext); return false; } return map_itr->second(filename, image.To(core::Device("CPU:0")), quality); diff --git a/cpp/open3d/t/io/file_format/FilePNG.cpp b/cpp/open3d/t/io/file_format/FilePNG.cpp index 3ac8f74d17b..ff8ef38e922 100644 --- a/cpp/open3d/t/io/file_format/FilePNG.cpp +++ b/cpp/open3d/t/io/file_format/FilePNG.cpp @@ -7,6 +7,7 @@ #include +#include "open3d/core/Dtype.h" #include "open3d/t/io/ImageIO.h" #include "open3d/utility/Logging.h" @@ -78,7 +79,8 @@ bool WriteImageToPNG(const std::string &filename, utility::LogWarning("Write PNG failed: image has no data."); return false; } - if (image.GetDtype() != core::UInt8 && image.GetDtype() != core::UInt16) { + if (image.GetDtype() != core::Bool && image.GetDtype() != core::UInt8 && + image.GetDtype() != core::UInt16) { utility::LogWarning("Write PNG failed: unsupported image data."); return false; } diff --git a/cpp/open3d/visualization/gui/Application.cpp b/cpp/open3d/visualization/gui/Application.cpp index 55ca48cd8ff..058733047c7 100644 --- a/cpp/open3d/visualization/gui/Application.cpp +++ b/cpp/open3d/visualization/gui/Application.cpp @@ -89,6 +89,8 @@ std::string FindResourcePath(int argc, const char *argv[]) { for (auto &subpath : {"/resources", "/../resources" /*building with Xcode */, "/share/resources" /* GNU */, "/share/Open3D/resources" /* GNU */}) { + open3d::utility::LogInfo("Checking for resources in {}", + path + subpath); if (o3dfs::DirectoryExists(path + subpath)) { return path + subpath; } diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index 3f56bb52d8b..e5df69fc48a 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -7,6 +7,8 @@ #include "open3d/t/geometry/TriangleMesh.h" +#include + #include #include @@ -1032,6 +1034,7 @@ the partition id for each face. triangle_mesh.def( "select_by_index", &TriangleMesh::SelectByIndex, "indices"_a, + "copy_attributes"_a = true, R"(Returns a new mesh with the vertices selected according to the indices list. If an item from the indices list exceeds the max vertex number of the mesh or has a negative value, it is ignored. @@ -1039,6 +1042,9 @@ or has a negative value, it is ignored. Args: indices (open3d.core.Tensor): An integer list of indices. Duplicates are allowed, but ignored. Signed and unsigned integral types are accepted. + copy_attributes (bool): Indicates if vertex attributes (other than + positions) and triangle attributes (other than indices) should be copied to + the returned mesh. Returns: A new mesh with the selected vertices and faces built from these vertices. @@ -1054,6 +1060,31 @@ or has a negative value, it is ignored. top_face = box.select_by_index([2, 3, 6, 7]) )"); + triangle_mesh.def("project_images_to_albedo", + &TriangleMesh::ProjectImagesToAlbedo, "images"_a, + "intrinsic_matrices"_a, "extrinsic_matrices"_a, + "tex_size"_a = 1024, "update_material"_a = true, + py::call_guard(), R"( +Create an albedo for the triangle mesh using calibrated images. The triangle +mesh must have texture coordinates ("texture_uvs" triangle attribute). This works +by back projecting the images onto the texture surface. Overlapping images are +blended together in the resulting albedo. For best results, use images captured +with exposure and white balance lock to reduce the chance of seams in the output +texture. + +Args: + images (List[open3d.t.geometry.Image]): List of images. + intrinsic_matrices (List[open3d.core.Tensor]): List of (3,3) intrinsic matrices describing + the pinhole camera. + extrinsic_matrices (List[open3d.core.Tensor]): List of (4,4) extrinsic matrices describing + the position and orientation of the camera. + tex_size (int): Output albedo texture size. This is a square image, so + only one side is needed. + update_material (bool): Whether to update the material of the triangle + mesh, possibly overwriting an existing albedo texture. + +Returns: + Image with albedo texture.)"); triangle_mesh.def( "remove_unreferenced_vertices", &TriangleMesh::RemoveUnreferencedVertices, diff --git a/cpp/pybind/visualization/o3dvisualizer.cpp b/cpp/pybind/visualization/o3dvisualizer.cpp index 89a09b73203..7da7cd1cbea 100644 --- a/cpp/pybind/visualization/o3dvisualizer.cpp +++ b/cpp/pybind/visualization/o3dvisualizer.cpp @@ -324,8 +324,8 @@ void pybind_o3dvisualizer_definitions(py::module& m) { "redraw is required.", "callback"_a) .def("export_current_image", &O3DVisualizer::ExportCurrentImage, - "Exports a PNG image of what is currently displayed to the " - "given path.", + "Exports a PNG or JPEG image of what is currently displayed " + "to the given path.", "path"_a) .def("start_rpc_interface", &O3DVisualizer::StartRPCInterface, "address"_a, "timeout"_a, diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index 7bca9b4a55e..9fd3837df84 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -8,13 +8,19 @@ #include "open3d/t/geometry/TriangleMesh.h" #include +#include #include "core/CoreTest.h" #include "open3d/core/Dtype.h" #include "open3d/core/EigenConverter.h" +#include "open3d/core/SizeVector.h" #include "open3d/core/Tensor.h" #include "open3d/core/TensorCheck.h" +#include "open3d/geometry/LineSet.h" #include "open3d/t/geometry/PointCloud.h" +#include "open3d/t/io/ImageIO.h" +#include "open3d/t/io/TriangleMeshIO.h" +#include "open3d/visualization/utility/Draw.h" #include "tests/Tests.h" namespace open3d { @@ -1345,6 +1351,55 @@ TEST_P(TriangleMeshPermuteDevices, RemoveUnreferencedVertices) { EXPECT_TRUE(torus.GetTriangleNormals().AllClose(expected_tri_normals)); } +TEST_P(TriangleMeshPermuteDevices, ProjectImagesToAlbedo) { + using namespace t::geometry; + using ::testing::ElementsAre; + using ::testing::FloatEq; + core::Device device = GetParam(); + TriangleMesh sphere = + TriangleMesh::FromLegacy(*geometry::TriangleMesh::CreateSphere( + 1.0, 20, /*create_uv_map=*/true)); + core::Tensor view[3] = {core::Tensor::Zeros({192, 256, 3}, core::Float32), + core::Tensor::Zeros({192, 256, 3}, core::Float32), + core::Tensor::Zeros({192, 256, 3}, core::Float32)}; + view[0].Slice(2, 0, 1, 1).Fill(1.0); // red + view[1].Slice(2, 1, 2, 1).Fill(1.0); // green + view[2].Slice(2, 2, 3, 1).Fill(1.0); // blue + core::Tensor intrinsic_matrix = core::Tensor::Init( + {{256, 0, 128}, {0, 256, 96}, {0, 0, 1}}, device); + core::Tensor extrinsic_matrix[3] = { + core::Tensor::Init( + {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 3}, {0, 0, 0, 1}}, + device), + core::Tensor::Init({{-0.5, 0, -0.8660254, 0}, + {0, 1, 0, 0}, + {0.8660254, 0, -0.5, 3}, + {0, 0, 0, 1}}, + device), + core::Tensor::Init({{-0.5, 0, 0.8660254, 0}, + {0, 1, 0, 0}, + {-0.8660254, 0, -0.5, 3}, + {0, 0, 0, 1}}, + device), + }; + + Image albedo = sphere.ProjectImagesToAlbedo( + {Image(view[0]), Image(view[1]), Image(view[2])}, + {intrinsic_matrix, intrinsic_matrix, intrinsic_matrix}, + {extrinsic_matrix[0], extrinsic_matrix[1], extrinsic_matrix[2]}, + 256, true); + + // visualization::Draw( + // {std::shared_ptr(&sphere, [](TriangleMesh*) {})}, + // "ProjectImagesToAlbedo", 1024, 768); + + EXPECT_THAT(albedo.AsTensor() + .To(core::Float32) + .Mean({0, 1}) + .ToFlatVector(), + ElementsAre(FloatEq(87.8693), FloatEq(67.538), FloatEq(64.31))); +} // namespace tests + TEST_P(TriangleMeshPermuteDevices, ComputeTriangleAreas) { core::Device device = GetParam(); t::geometry::TriangleMesh mesh_empty; diff --git a/docs/tutorial/data/index.rst b/docs/tutorial/data/index.rst index e17c8d4619e..f1fcf2dd21c 100644 --- a/docs/tutorial/data/index.rst +++ b/docs/tutorial/data/index.rst @@ -175,13 +175,13 @@ A 3D Mobius knot mesh in PLY format. data::KnotMesh dataset; auto mesh = io::CreateMeshFromFile(dataset.GetPath()); -TriangleModel with PRB texture +TriangleModel with PBR texture ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MonkeyModel ----------- -The monkey model with PRB texture. +The monkey model with PBR texture. .. code-block:: python @@ -197,7 +197,7 @@ The monkey model with PRB texture. SwordModel ---------- -The sword model with PRB texture. +The sword model with PBR texture. .. code-block:: python @@ -213,7 +213,7 @@ The sword model with PRB texture. CrateModel ---------- -The crate model with PRB texture. +The crate model with PBR texture. .. code-block:: python @@ -229,7 +229,7 @@ The crate model with PRB texture. FlightHelmetModel ----------------- -The flight helmet gltf model with PRB texture. +The flight helmet gltf model with PBR texture. .. code-block:: python diff --git a/examples/python/geometry/triangle_mesh_project_to_albedo.py b/examples/python/geometry/triangle_mesh_project_to_albedo.py new file mode 100644 index 00000000000..37204aa47fe --- /dev/null +++ b/examples/python/geometry/triangle_mesh_project_to_albedo.py @@ -0,0 +1,203 @@ +# ---------------------------------------------------------------------------- +# - Open3D: www.open3d.org - +# ---------------------------------------------------------------------------- +# Copyright (c) 2018-2024 www.open3d.org +# SPDX-License-Identifier: MIT +# ---------------------------------------------------------------------------- +"""This example demonstrates project_image_to_albedo. Use create_dataset mode to +render images of a 3D mesh or model from different viewpoints. +albedo_from_dataset mode then uses the calibrated images to re-create the albedo +texture for the mesh. +""" +import argparse +from pathlib import Path +import subprocess as sp +import time +import numpy as np +import open3d as o3d +from open3d.visualization import gui, rendering, O3DVisualizer +from open3d.core import Tensor + + +def download_smithsonian_baluster_vase(): + """Download the Smithsonian Baluster Vase 3D model.""" + vase_url = 'https://3d-api.si.edu/content/document/3d_package:d8c62634-4ebc-11ea-b77f-2e728ce88125/resources/F1980.190%E2%80%93194_baluster_vase-150k-4096.glb' + import urllib.request + + def show_progress(block_num, block_size, total_size): + total_size = total_size >> 20 if total_size > 0 else "??" # Convert to MB if known + print( + "Downloading F1980_baluster_vase.glb... " + f"{(block_num * block_size) >>20}MB / {total_size}MB", + end="\r") + + urllib.request.urlretrieve(vase_url, + filename="F1980_baluster_vase.glb", + reporthook=show_progress) + print("\nDownload complete.") + + +def create_dataset(meshfile, n_images=10, movie=False, vary_exposure=False): + """Render images of a 3D mesh from different viewpoints, covering the + northern hemisphere. These form a synthetic dataset to test the + project_images_to_albedo function. + """ + # Adjust these parameters to properly frame your model. + # Window system pixel scaling (e.g. 1 for normal, 2 for HiDPI / retina display) + SCALING = 2 + width, height = 1024, 1024 # image width, height + focal_length = 512 + d_camera_obj = 0.3 # distance from camera to object + K = np.array([[focal_length, 0, width / 2], [0, focal_length, height / 2], + [0, 0, 1]]) + t = np.array([0, 0, d_camera_obj]) # origin / object in camera ref frame + + model = o3d.io.read_triangle_model(meshfile) + # DefaultLit shader will produce non-uniform images with specular + # highlights, etc. These should be avoided to accurately capture the diffuse + # albedo + unlit = rendering.MaterialRecord() + unlit.shader = "unlit" + + def triangle_wave(n, period=1): + """Triangle wave function between [0,1] with given period.""" + return abs(n % period - period / 2) / (period / 2) + + def rotate_camera_and_shoot(o3dvis): + Rts = [] + images = [] + o3dvis.scene.scene.enable_sun_light(False) + print("Rendering images: ", end='', flush=True) + n_0 = 2 * n_images // 3 + n_1 = n_images - n_0 - 1 + for n in range(n_images): + Rt = np.eye(4) + Rt[:3, 3] = t + if n < n_0: + theta = n * (2 * np.pi) / n_0 + Rt[:3, : + 3] = o3d.geometry.Geometry3D.get_rotation_matrix_from_zyx( + [np.pi, theta, 0]) + elif n < n_images - 1: + theta = (n - n_0) * (2 * np.pi) / n_1 + Rt[:3, : + 3] = o3d.geometry.Geometry3D.get_rotation_matrix_from_xyz( + [np.pi / 4, theta, np.pi]) + else: # one image from the top + Rt[:3, : + 3] = o3d.geometry.Geometry3D.get_rotation_matrix_from_zyx( + [np.pi, 0, -np.pi / 2]) + Rts.append(Rt) + o3dvis.setup_camera(K, Rt, width, height) + # Vary IBL intensity as a poxy for exposure value. IBL ranges from + # [0,150000]. We vary it between 20000 and 100000. + if vary_exposure: + o3dvis.set_ibl_intensity(20000 + + 80000 * triangle_wave(n, n_images / 4)) + o3dvis.post_redraw() + o3dvis.export_current_image(f"render-{n:02}.jpg") + images.append(f"render-{n:02}.jpg") + print('.', end='', flush=True) + np.savez("cameras.npz", + width=width, + height=height, + K=K, + Rts=Rts, + images=images) + # Now create a movie from the saved images by calling ffmpeg with + # subprocess + if movie: + print("\nCreating movie...", end='', flush=True) + sp.run([ + "ffmpeg", "-framerate", f"{n_images/6}", "-pattern_type", + "glob", "-i", "render-*.jpg", "-y", meshfile.stem + ".mp4" + ], + check=True) + o3dvis.close() + print("\nDone.") + + print("If the object is properly framed in the GUI window, click on the " + "'Save Images' action in the menu.") + o3d.visualization.draw([{ + 'geometry': model, + 'name': meshfile.name, + 'material': unlit + }], + show_ui=False, + width=int(width / SCALING), + height=int(height / SCALING), + actions=[("Save Images", rotate_camera_and_shoot)]) + + +def albedo_from_images(meshfile, calib_data_file, albedo_contrast=1.25): + + model = o3d.io.read_triangle_model(meshfile) + tmeshes = o3d.t.geometry.TriangleMesh.from_triangle_mesh_model(model) + tmeshes = list(tmeshes.values()) + calib = np.load(calib_data_file) + Ks = list(Tensor(calib["K"]) for _ in range(len(calib["Rts"]))) + Rts = list(Tensor(Rt) for Rt in calib["Rts"]) + images = list(o3d.t.io.read_image(imfile) for imfile in calib["images"]) + calib.close() + start = time.time() + with o3d.utility.VerbosityContextManager(o3d.utility.VerbosityLevel.Debug): + albedo = tmeshes[0].project_images_to_albedo(images, Ks, Rts, 1024, + True) + albedo = albedo.linear_transform(scale=albedo_contrast) # brighten albedo + tmeshes[0].material.texture_maps["albedo"] = albedo + print(f"project_images_to_albedo ran in {time.time()-start:.2f}s") + o3d.t.io.write_image("albedo.png", albedo) + o3d.t.io.write_triangle_mesh(meshfile.stem + "_albedo.glb", tmeshes[0]) + cam_vis = list({ + "name": + f"camera-{i:02}", + "geometry": + o3d.geometry.LineSet.create_camera_visualization( + images[0].columns, images[0].rows, K.numpy(), Rt.numpy(), 0.1) + } for i, (K, Rt) in enumerate(zip(Ks, Rts))) + o3d.visualization.draw(cam_vis + [{ + "name": meshfile.name, + "geometry": tmeshes[0] + }], + show_ui=True) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", + choices=('create_dataset', 'albedo_from_images')) + parser.add_argument("--meshfile", + type=Path, + default=".", + help="Path to mesh file.") + parser.add_argument("--n-images", + type=int, + default=10, + help="Number of images to render.") + parser.add_argument("--download_sample_model", + help="Download a sample 3D model for this example.", + action="store_true") + parser.add_argument( + "--movie", + action="store_true", + help= + "Create movie from rendered images with ffmpeg. ffmpeg must be installed and in path." + ) + args = parser.parse_args() + + if args.action == "create_dataset": + if args.download_sample_model: + download_smithsonian_baluster_vase() + args.meshfile = "F1980_baluster_vase.glb" + if args.meshfile == Path("."): + parser.error("Please provide a path to a mesh file, or use " + "--download_sample_model.") + if args.n_images < 10: + parser.error("Atleast 10 images should be used!") + create_dataset(args.meshfile, + n_images=args.n_images, + movie=args.movie, + vary_exposure=True) + else: + albedo_from_images(args.meshfile, "cameras.npz") diff --git a/examples/python/visualization/remove_geometry.py b/examples/python/visualization/remove_geometry.py index 42b7af1fbe8..5071e091448 100644 --- a/examples/python/visualization/remove_geometry.py +++ b/examples/python/visualization/remove_geometry.py @@ -38,7 +38,7 @@ def visualize_non_blocking(vis, pcds): curr_sec = int(time.time() - start_time) prev_sec = curr_sec - 1 -while True: +while curr_sec < 10: curr_sec = int(time.time() - start_time) if curr_sec - prev_sec == 1: prev_sec = curr_sec @@ -54,3 +54,4 @@ def visualize_non_blocking(vis, pcds): print("Removing %d" % i) visualize_non_blocking(vis, pcds) + time.sleep(0.025) # yield CPU to others while maintaining responsiveness diff --git a/python/open3d/visualization/draw.py b/python/open3d/visualization/draw.py index 57b1a7cbdb2..c9047f4b93c 100644 --- a/python/open3d/visualization/draw.py +++ b/python/open3d/visualization/draw.py @@ -93,12 +93,12 @@ def draw(geometry=None, on_animation_frame (Callable): Callback for each animation frame update with signature:: - Callback(O3DVisualizer, double time) -> None + Callback(O3DVisualizer o3dvis, double time) -> None on_animation_tick (Callable): Callback for each animation time step with signature:: - Callback(O3DVisualizer, double tick_duration, double time) -> TickResult + Callback(O3DVisualizer o3dvis, double tick_duration, double time) -> TickResult If the callback returns ``TickResult.REDRAW``, the scene is redrawn. It should return ``TickResult.NOCHANGE`` if redraw is not required. @@ -153,7 +153,7 @@ def toggle_result(o3dvis): ("Toggle truth/result", toggle_result)]) """ gui.Application.instance.initialize() - w = O3DVisualizer(title, width, height) + w = O3DVisualizer(title, int(width), int(height)) w.set_background(bg_color, bg_image) if actions is not None: