mirror of
https://github.com/opencv/opencv.git
synced 2026-11-09 18:04:29 -06:00
Merge pull request #29729 from SavyaSanchi-Sharma:vizsplat
Added Gaussian Splat Renderer Support - #29729 Adds 3D Gaussian Splatting support to `ptcloud`: a loader for trained scenes and a `viz3d` renderer that draws them alpha-blended and depth-sorted alongside existing opaque geometry. ### Result <img width="1842" height="811" alt="splat" src="https://github.com/user-attachments/assets/abfa5c01-6dbb-4367-b426-a618e3748649" /> [.splat source](https://huggingface.co/cakewalk/splat-data/resolve/main/train.splat) ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
committed by
GitHub
parent
7510afe588
commit
303f83ef92
@@ -86,7 +86,8 @@ public:
|
||||
ARRAY_BUFFER = 0x8892, //!< The buffer will be used as a source for vertex data
|
||||
ELEMENT_ARRAY_BUFFER = 0x8893, //!< The buffer will be used for indices (in glDrawElements, for example)
|
||||
PIXEL_PACK_BUFFER = 0x88EB, //!< The buffer will be used for reading from OpenGL textures
|
||||
PIXEL_UNPACK_BUFFER = 0x88EC //!< The buffer will be used for writing to OpenGL textures
|
||||
PIXEL_UNPACK_BUFFER = 0x88EC, //!< The buffer will be used for writing to OpenGL textures
|
||||
TEXTURE_BUFFER = 0x8C2A //!< The buffer will be sampled by a shader through ogl::TextureBuffer
|
||||
};
|
||||
|
||||
enum Access
|
||||
@@ -399,6 +400,102 @@ private:
|
||||
Format format_;
|
||||
};
|
||||
|
||||
/** @brief Smart pointer for an OpenGL buffer texture with reference counting.
|
||||
|
||||
A buffer texture is a read-only, one-dimensional view of an ogl::Buffer that a shader samples with
|
||||
`samplerBuffer` (float formats) or `isamplerBuffer` (integer formats). Unlike ogl::Texture2D it has
|
||||
no filtering, no mipmaps and no normalized coordinates: the shader indexes it by element with
|
||||
`texelFetch`. It is the way to feed a shader more data than uniforms can hold.
|
||||
|
||||
The object keeps a reference to the source buffer, so the buffer stays alive as long as the texture
|
||||
does. The attachment records the buffer's id at create() time, so re-creating or resizing the source
|
||||
buffer needs a matching create() call to re-attach.
|
||||
|
||||
Requires OpenGL 3.1 or later.
|
||||
*/
|
||||
class CV_EXPORTS TextureBuffer
|
||||
{
|
||||
public:
|
||||
/** @brief The internal format the shader sees, i.e. component count and type per element.
|
||||
*/
|
||||
enum Format
|
||||
{
|
||||
NONE = 0,
|
||||
R32F = 0x822E, //!< 1 x 32-bit float
|
||||
RG32F = 0x8230, //!< 2 x 32-bit float
|
||||
RGBA32F = 0x8814, //!< 4 x 32-bit float
|
||||
R32I = 0x8235, //!< 1 x 32-bit signed int
|
||||
RG32I = 0x823B, //!< 2 x 32-bit signed int
|
||||
RGBA32I = 0x8D82 //!< 4 x 32-bit signed int
|
||||
};
|
||||
|
||||
/** @brief The constructors.
|
||||
|
||||
Creates an empty ogl::TextureBuffer object or attaches one to an existing buffer.
|
||||
*/
|
||||
TextureBuffer();
|
||||
|
||||
/** @overload
|
||||
@param buf Source buffer, must be non-empty. Should have been created with the
|
||||
ogl::Buffer::TEXTURE_BUFFER target.
|
||||
@param aformat Element format. See cv::ogl::TextureBuffer::Format .
|
||||
@param autoRelease Auto release mode (if true, release will be called in object's destructor).
|
||||
*/
|
||||
TextureBuffer(const Buffer& buf, Format aformat, bool autoRelease = false);
|
||||
|
||||
/** @brief Attaches the texture to an OpenGL buffer.
|
||||
|
||||
@param buf Source buffer, must be non-empty. Should have been created with the
|
||||
ogl::Buffer::TEXTURE_BUFFER target.
|
||||
@param aformat Element format, must not be NONE. See cv::ogl::TextureBuffer::Format .
|
||||
@param autoRelease Auto release mode (if true, release will be called in object's destructor).
|
||||
|
||||
No data is copied - the texture reads whatever the buffer holds at draw time, so writing to the
|
||||
buffer is enough to update what the shader samples.
|
||||
*/
|
||||
void create(const Buffer& buf, Format aformat, bool autoRelease = false);
|
||||
|
||||
/** @brief Detaches from the buffer and destroys the texture object if needed.
|
||||
|
||||
The function will call setAutoRelease(true) .
|
||||
*/
|
||||
void release();
|
||||
|
||||
/** @brief Sets auto release mode.
|
||||
|
||||
@param flag Auto release mode (if true, release will be called in object's destructor).
|
||||
|
||||
The lifetime of the OpenGL object is tied to the lifetime of the context, so ogl::TextureBuffer
|
||||
doesn't destroy the OpenGL object in its destructor by default. See
|
||||
ogl::Texture2D::setAutoRelease for the full rationale.
|
||||
*/
|
||||
void setAutoRelease(bool flag);
|
||||
|
||||
/** @brief Binds the texture to a texture unit for the GL_TEXTURE_BUFFER target.
|
||||
|
||||
@param unit Texture unit index to bind to, must be non-negative. Pass the same index to the
|
||||
shader's sampler uniform.
|
||||
|
||||
Leaves that unit active, so bind the unit a caller expects to stay current last.
|
||||
*/
|
||||
void bind(int unit = 0) const;
|
||||
|
||||
//! element format, NONE if not attached
|
||||
Format format() const;
|
||||
//! true if the texture is not attached to any buffer
|
||||
bool empty() const;
|
||||
|
||||
//! get OpenGL object id
|
||||
unsigned int texId() const;
|
||||
|
||||
class Impl;
|
||||
|
||||
private:
|
||||
Ptr<Impl> impl_;
|
||||
Buffer buf_;
|
||||
Format format_;
|
||||
};
|
||||
|
||||
/** @brief Wrapper for OpenGL Client-Side Vertex arrays.
|
||||
|
||||
ogl::Arrays stores vertex data in ogl::Buffer objects.
|
||||
@@ -695,12 +792,36 @@ public:
|
||||
*/
|
||||
static void setUniformVec3(int location, Vec3f vec);
|
||||
|
||||
/** @brief Sets a uniform vector value.
|
||||
@param location Uniform location.
|
||||
@param vec Vector value.
|
||||
*/
|
||||
static void setUniformVec2(int location, Vec2f vec);
|
||||
|
||||
/** @brief Sets a uniform vector value.
|
||||
@param location Uniform location.
|
||||
@param vec Vector value.
|
||||
*/
|
||||
static void setUniformVec4(int location, Vec4f vec);
|
||||
|
||||
/** @brief Sets a uniform matrix value.
|
||||
@param location Uniform location.
|
||||
@param mat Matrix value.
|
||||
*/
|
||||
static void setUniformMat4x4(int location, Matx44f mat);
|
||||
|
||||
/** @brief Sets a uniform scalar value.
|
||||
@param location Uniform location.
|
||||
@param value Float value.
|
||||
*/
|
||||
static void setUniform1f(int location, float value);
|
||||
|
||||
/** @brief Sets a uniform scalar value.
|
||||
@param location Uniform location.
|
||||
@param value Integer value. Also used for sampler uniforms, where it is the texture unit index.
|
||||
*/
|
||||
static void setUniform1i(int location, int value);
|
||||
|
||||
//! get OpenGL object id
|
||||
unsigned int programId() const;
|
||||
|
||||
@@ -736,6 +857,22 @@ enum IndexType {
|
||||
//! capability
|
||||
enum Capability {
|
||||
DEPTH_TEST = 0x0B71,
|
||||
CULL_FACE = 0x0B44,
|
||||
BLEND = 0x0BE2,
|
||||
};
|
||||
|
||||
//! blend factor
|
||||
enum BlendFactor {
|
||||
BLEND_ZERO = 0,
|
||||
BLEND_ONE = 1,
|
||||
BLEND_SRC_COLOR = 0x0300,
|
||||
BLEND_ONE_MINUS_SRC_COLOR = 0x0301,
|
||||
BLEND_SRC_ALPHA = 0x0302,
|
||||
BLEND_ONE_MINUS_SRC_ALPHA = 0x0303,
|
||||
BLEND_DST_ALPHA = 0x0304,
|
||||
BLEND_ONE_MINUS_DST_ALPHA = 0x0305,
|
||||
BLEND_DST_COLOR = 0x0306,
|
||||
BLEND_ONE_MINUS_DST_COLOR = 0x0307,
|
||||
};
|
||||
|
||||
/** @brief Render OpenGL texture or primitives.
|
||||
@@ -787,6 +924,26 @@ CV_EXPORTS void drawArrays(int first, int count, int mode);
|
||||
*/
|
||||
CV_EXPORTS void drawElements(int first, int count, int type, int mode);
|
||||
|
||||
/** @brief Renders multiple instances of primitives from array data with index buffer set.
|
||||
@param first Byte offset of the first index in the bound element buffer.
|
||||
@param count Number of indices to draw.
|
||||
@param type Index type. See cv::ogl::IndexType.
|
||||
@param mode Rendering mode. See cv::ogl::RenderModes.
|
||||
@param instanceCount Number of instances to draw. Each instance sees its own gl_InstanceID.
|
||||
*/
|
||||
CV_EXPORTS void drawElementsInstanced(int first, int count, int type, int mode, int instanceCount);
|
||||
|
||||
/** @brief Sets the blend function for the current target.
|
||||
@param sfactor Source blend factor. See cv::ogl::BlendFactor.
|
||||
@param dfactor Destination blend factor. See cv::ogl::BlendFactor.
|
||||
*/
|
||||
CV_EXPORTS void blendFunc(int sfactor, int dfactor);
|
||||
|
||||
/** @brief Enables or disables writing to the depth buffer.
|
||||
@param flag If false, depth testing still happens but no depth value is written.
|
||||
*/
|
||||
CV_EXPORTS void depthMask(bool flag);
|
||||
|
||||
/** @brief Enable server-side GL capabilities.
|
||||
@param cap The capability to enable.
|
||||
*/
|
||||
|
||||
@@ -1253,6 +1253,172 @@ unsigned int cv::ogl::Texture2D::texId() const
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ogl::TextureBuffer
|
||||
|
||||
#ifndef HAVE_OPENGL
|
||||
|
||||
class cv::ogl::TextureBuffer::Impl
|
||||
{
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
class cv::ogl::TextureBuffer::Impl
|
||||
{
|
||||
public:
|
||||
static const Ptr<Impl> empty();
|
||||
|
||||
Impl();
|
||||
Impl(GLenum internalFormat, GLuint bufId, bool autoRelease);
|
||||
~Impl();
|
||||
|
||||
void bind(GLint unit) const;
|
||||
|
||||
void setAutoRelease(bool flag) { autoRelease_ = flag; }
|
||||
|
||||
GLuint texId() const { return texId_; }
|
||||
|
||||
private:
|
||||
GLuint texId_;
|
||||
bool autoRelease_;
|
||||
};
|
||||
|
||||
const Ptr<cv::ogl::TextureBuffer::Impl> cv::ogl::TextureBuffer::Impl::empty()
|
||||
{
|
||||
static Ptr<Impl> p = makePtr<Impl>();
|
||||
return p;
|
||||
}
|
||||
|
||||
cv::ogl::TextureBuffer::Impl::Impl() : texId_(0), autoRelease_(false)
|
||||
{
|
||||
}
|
||||
|
||||
cv::ogl::TextureBuffer::Impl::Impl(GLenum internalFormat, GLuint bufId, bool autoRelease) : texId_(0), autoRelease_(autoRelease)
|
||||
{
|
||||
gl::GenTextures(1, &texId_);
|
||||
CV_CheckGlError();
|
||||
|
||||
CV_Assert(texId_ != 0);
|
||||
|
||||
gl::BindTexture(gl::TEXTURE_BUFFER, texId_);
|
||||
CV_CheckGlError();
|
||||
|
||||
gl::TexBuffer(gl::TEXTURE_BUFFER, internalFormat, bufId);
|
||||
CV_CheckGlError();
|
||||
|
||||
gl::BindTexture(gl::TEXTURE_BUFFER, 0);
|
||||
CV_CheckGlError();
|
||||
}
|
||||
|
||||
cv::ogl::TextureBuffer::Impl::~Impl()
|
||||
{
|
||||
if (autoRelease_ && texId_)
|
||||
gl::DeleteTextures(1, &texId_);
|
||||
}
|
||||
|
||||
void cv::ogl::TextureBuffer::Impl::bind(GLint unit) const
|
||||
{
|
||||
gl::ActiveTexture(gl::TEXTURE0 + unit);
|
||||
CV_CheckGlError();
|
||||
|
||||
gl::BindTexture(gl::TEXTURE_BUFFER, texId_);
|
||||
CV_CheckGlError();
|
||||
}
|
||||
|
||||
#endif // HAVE_OPENGL
|
||||
|
||||
cv::ogl::TextureBuffer::TextureBuffer() : format_(NONE)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
throw_no_ogl();
|
||||
#else
|
||||
impl_ = Impl::empty();
|
||||
#endif
|
||||
}
|
||||
|
||||
cv::ogl::TextureBuffer::TextureBuffer(const Buffer& buf, Format aformat, bool autoRelease) : format_(NONE)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(buf);
|
||||
CV_UNUSED(aformat);
|
||||
CV_UNUSED(autoRelease);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
impl_ = Impl::empty();
|
||||
create(buf, aformat, autoRelease);
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::TextureBuffer::create(const Buffer& buf, Format aformat, bool autoRelease)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(buf);
|
||||
CV_UNUSED(aformat);
|
||||
CV_UNUSED(autoRelease);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
CV_Assert( !buf.empty() );
|
||||
CV_Assert( aformat != NONE );
|
||||
|
||||
impl_ = makePtr<Impl>(aformat, buf.bufId(), autoRelease);
|
||||
buf_ = buf;
|
||||
format_ = aformat;
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::TextureBuffer::release()
|
||||
{
|
||||
#ifdef HAVE_OPENGL
|
||||
if (impl_)
|
||||
impl_->setAutoRelease(true);
|
||||
impl_ = Impl::empty();
|
||||
#endif
|
||||
buf_ = Buffer();
|
||||
format_ = NONE;
|
||||
}
|
||||
|
||||
void cv::ogl::TextureBuffer::setAutoRelease(bool flag)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(flag);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
impl_->setAutoRelease(flag);
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::TextureBuffer::bind(int unit) const
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(unit);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
CV_Assert( unit >= 0 );
|
||||
impl_->bind(unit);
|
||||
#endif
|
||||
}
|
||||
|
||||
cv::ogl::TextureBuffer::Format cv::ogl::TextureBuffer::format() const
|
||||
{
|
||||
return format_;
|
||||
}
|
||||
|
||||
bool cv::ogl::TextureBuffer::empty() const
|
||||
{
|
||||
return format_ == NONE;
|
||||
}
|
||||
|
||||
unsigned int cv::ogl::TextureBuffer::texId() const
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
throw_no_ogl();
|
||||
#else
|
||||
return impl_->texId();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// ogl::Arrays
|
||||
|
||||
@@ -1959,6 +2125,26 @@ void cv::ogl::Program::setUniformVec3(int loc, Vec3f vec)
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::Program::setUniformVec2(int loc, Vec2f vec)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(loc); CV_UNUSED(vec);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::Uniform2f(loc, vec(0), vec(1));
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::Program::setUniformVec4(int loc, Vec4f vec)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(loc); CV_UNUSED(vec);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::Uniform4f(loc, vec(0), vec(1), vec(2), vec(3));
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::Program::setUniformMat4x4(int loc, Matx44f mat)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
@@ -1969,6 +2155,26 @@ void cv::ogl::Program::setUniformMat4x4(int loc, Matx44f mat)
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::Program::setUniform1f(int loc, float value)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(loc); CV_UNUSED(value);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::Uniform1f(loc, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::Program::setUniform1i(int loc, int value)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(loc); CV_UNUSED(value);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::Uniform1i(loc, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
unsigned int cv::ogl::Program::programId() const
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
@@ -2187,6 +2393,37 @@ void cv::ogl::drawElements(int first, int count, int type, int mode)
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::drawElementsInstanced(int first, int count, int type, int mode, int instanceCount)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(first); CV_UNUSED(count); CV_UNUSED(type); CV_UNUSED(mode); CV_UNUSED(instanceCount);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::DrawElementsInstanced(mode, count, type, reinterpret_cast<const void*>(static_cast<size_t>(first)), instanceCount);
|
||||
CV_CheckGlError();
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::blendFunc(int sfactor, int dfactor)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(sfactor); CV_UNUSED(dfactor);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::BlendFunc(sfactor, dfactor);
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::depthMask(bool flag)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(flag);
|
||||
throw_no_ogl();
|
||||
#else
|
||||
gl::DepthMask(flag ? gl::TRUE_ : gl::FALSE_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void cv::ogl::enable(int cap)
|
||||
{
|
||||
#ifndef HAVE_OPENGL
|
||||
|
||||
@@ -911,6 +911,11 @@ namespace
|
||||
CV_Error(cv::Error::OpenGlApiCallError, "OpenGL context is not initialized");
|
||||
return FALSE;
|
||||
}
|
||||
const int scale = gtk_widget_get_scale_factor(GTK_WIDGET(area));
|
||||
glViewport(0, 0,
|
||||
gtk_widget_get_allocated_width(GTK_WIDGET(area)) * scale,
|
||||
gtk_widget_get_allocated_height(GTK_WIDGET(area)) * scale);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
if(window->glDrawCallback) {
|
||||
window->glDrawCallback(window->glDrawData);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,72 @@ CV_EXPORTS_W void orientedBoundingBox3D(InputArray inputCloud, OutputArray cente
|
||||
*/
|
||||
CV_EXPORTS_W double approxEnclosingSphere3D(InputArray inputCloud, OutputArray center);
|
||||
|
||||
/** @brief Column layout of a decoded 3D Gaussian Splatting scene.
|
||||
*
|
||||
* A decoded scene is a CV_32F matrix with one row per Gaussian and #GAUSSIAN_SPLAT_STRIDE columns.
|
||||
*/
|
||||
enum GaussianSplatLayout
|
||||
{
|
||||
GAUSSIAN_SPLAT_STRIDE = 13, //!< Floats per Gaussian, i.e. the width of a decoded scene.
|
||||
GAUSSIAN_SPLAT_POS = 0, //!< First column of the position, 3 floats: x, y, z.
|
||||
GAUSSIAN_SPLAT_COV = 3, //!< First column of the covariance upper triangle, 6 floats: xx, xy, xz, yy, yz, zz.
|
||||
GAUSSIAN_SPLAT_RGB = 9, //!< First column of the color, 3 floats in [0, 1].
|
||||
GAUSSIAN_SPLAT_ALPHA = 12 //!< Column of the opacity, 1 float in [0, 1].
|
||||
};
|
||||
|
||||
/** @brief Returns the PLY vertex property names cv::loadGaussianSplats reads by default.
|
||||
*
|
||||
* The order of the names is the column order cv::decodeGaussianSplats expects. Pass a renamed copy
|
||||
* of this list to cv::loadGaussianSplats to read a scene whose exporter spells the properties
|
||||
* differently, keeping the same order.
|
||||
*/
|
||||
CV_EXPORTS_W std::vector<String> getGaussianSplatPlyPropNames();
|
||||
|
||||
/** @brief Decodes 3D Gaussian Splatting attributes into a renderable scene.
|
||||
*
|
||||
* Scales are exponentiated, opacity is put through a sigmoid, the quaternion and scales are
|
||||
* combined into a 3D covariance, and the zeroth order harmonic is evaluated to an RGB color.
|
||||
*
|
||||
* Use this when the attributes do not come from a file cv::loadGaussianSplats can open, for
|
||||
* instance when they are held in memory, arrive from a training run, or are read from a resource
|
||||
* on a platform without filesystem access.
|
||||
*
|
||||
* @param attributes CV_32F matrix with one row per Gaussian and one column per name of
|
||||
* cv::getGaussianSplatPlyPropNames, in that order.
|
||||
* @param splats Decoded scene, see #GaussianSplatLayout.
|
||||
*/
|
||||
CV_EXPORTS_W void decodeGaussianSplats(InputArray attributes, OutputArray splats);
|
||||
|
||||
/** @brief Decodes the contents of a `.splat` file into a renderable scene.
|
||||
*
|
||||
* A `.splat` file is a headerless array of 32 byte records, one per Gaussian, holding position,
|
||||
* scale, color and rotation. The values are already activated, so only the covariance is built.
|
||||
* Being headerless makes this the in memory counterpart of cv::loadGaussianSplats for platforms
|
||||
* without filesystem access, or for scenes shipped as application resources.
|
||||
*
|
||||
* @param buf CV_8U buffer holding the bytes of a `.splat` file.
|
||||
* @param splats Decoded scene, see #GaussianSplatLayout.
|
||||
*/
|
||||
CV_EXPORTS_W void decodeGaussianSplatsPacked(InputArray buf, OutputArray splats);
|
||||
|
||||
/** @brief Loads a 3D Gaussian Splatting scene from a PLY or `.splat` file.
|
||||
*
|
||||
* Requires a trained scene, i.e. a PLY whose vertices carry `f_dc_0..2`, `opacity`, `scale_0..2`
|
||||
* and `rot_0..3` alongside `x`, `y`, `z`, or a `.splat` file of 32 byte records. Higher order
|
||||
* `f_rest_*` harmonics are ignored.
|
||||
*
|
||||
* The attributes are decoded on load, see cv::decodeGaussianSplats and
|
||||
* cv::decodeGaussianSplatsPacked.
|
||||
*
|
||||
* @param filename Name of the file, the format is chosen from its extension.
|
||||
* @param splats Decoded scene in the layout cv::viz3d::showGaussianSplats expects, see
|
||||
* #GaussianSplatLayout, or empty on failure.
|
||||
* @param ply_properties Vertex property names to read from a PLY file, in the column order of
|
||||
* cv::getGaussianSplatPlyPropNames. Empty selects that default list. Unused for `.splat` files.
|
||||
*/
|
||||
CV_EXPORTS_W void loadGaussianSplats(const String &filename, OutputArray splats,
|
||||
const std::vector<String> &ply_properties = std::vector<String>());
|
||||
|
||||
/** @brief Loads a mesh from a file.
|
||||
*
|
||||
* The function loads mesh from the specified file and returns it.
|
||||
|
||||
@@ -137,6 +137,23 @@ each row represents a point.
|
||||
*/
|
||||
CV_EXPORTS_W void showPoints(const String& win_name, const String& obj_name, InputArray points);
|
||||
|
||||
/** @brief Shows a 3D Gaussian Splatting scene in the specified viz3d window. See cv::viz3d::destroyObject.
|
||||
|
||||
Splats are alpha blended, sorted back to front by camera-space depth on the CPU whenever the view
|
||||
changes, and drawn after all opaque objects in the same window.
|
||||
|
||||
Several splat objects in one window are drawn back to front by the depth of their centers. This is
|
||||
correct for scenes that are apart from each other, but scenes that interpenetrate can blend in the
|
||||
wrong order where they overlap. Merge such scenes into a single object instead.
|
||||
|
||||
@param win_name Name of the viz3d window.
|
||||
@param obj_name Name of the object.
|
||||
@param splats One row per Gaussian, 13 floats wide: position (3), upper triangle of the symmetric
|
||||
3D covariance (6: xx, xy, xz, yy, yz, zz), color (3), opacity (1). As produced by
|
||||
cv::loadGaussianSplats or cv::decodeGaussianSplats, see cv::GaussianSplatLayout.
|
||||
*/
|
||||
CV_EXPORTS_W void showGaussianSplats(const String& win_name, const String& obj_name, InputArray splats);
|
||||
|
||||
/** @brief Shows a colored depth map as a point cloud in the specified viz3d window. See cv::viz3d::destroyObject.
|
||||
|
||||
The colored depth map must be a 2D image with 4 channels (RGBD). The points are created assuming
|
||||
|
||||
@@ -422,6 +422,87 @@ uchar readNext<uchar>(std::ifstream &file, DataFormat format)
|
||||
return val;
|
||||
}
|
||||
|
||||
static float readPropertyValue(std::ifstream &file, int valType, DataFormat format)
|
||||
{
|
||||
switch (valType)
|
||||
{
|
||||
case CV_8U:
|
||||
return (float)readNext<uchar>(file, format);
|
||||
case CV_8S:
|
||||
return (float)(schar)readNext<uchar>(file, format);
|
||||
case CV_16U:
|
||||
return (float)readNext<ushort>(file, format);
|
||||
case CV_16S:
|
||||
return (float)(short)readNext<ushort>(file, format);
|
||||
case CV_32U:
|
||||
return (float)readNext<uint>(file, format);
|
||||
case CV_32S:
|
||||
return (float)(int)readNext<uint>(file, format);
|
||||
case CV_32F:
|
||||
return readNext<float>(file, format);
|
||||
case CV_64F:
|
||||
return (float)readNext<double>(file, format);
|
||||
default:
|
||||
return 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlyDecoder::readNamedProperties(const std::vector<std::string>& names, Mat& values)
|
||||
{
|
||||
values.release();
|
||||
|
||||
std::ifstream file(m_filename, std::ios::binary);
|
||||
int nTexCoords = 0;
|
||||
if (!parseHeader(file, nTexCoords))
|
||||
return false;
|
||||
|
||||
const size_t nProps = m_vertexDescription.properties.size();
|
||||
std::vector<int> colOf(nProps, -1);
|
||||
std::vector<bool> got(names.size(), false);
|
||||
|
||||
for (size_t j = 0; j < nProps; j++)
|
||||
{
|
||||
for (size_t k = 0; k < names.size(); k++)
|
||||
{
|
||||
if (m_vertexDescription.properties[j].name == names[k] && !got[k])
|
||||
{
|
||||
colOf[j] = (int)k;
|
||||
got[k] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < names.size(); k++)
|
||||
{
|
||||
if (!got[k])
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex property " << names[k] << " is not present in the file");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
values.create((int)m_vertexCount, (int)names.size(), CV_32F);
|
||||
for (size_t i = 0; i < m_vertexCount; i++)
|
||||
{
|
||||
float* row = values.ptr<float>((int)i);
|
||||
for (size_t j = 0; j < nProps; j++)
|
||||
{
|
||||
float v = readPropertyValue(file, m_vertexDescription.properties[j].valType, m_inputDataFormat);
|
||||
if (colOf[j] >= 0)
|
||||
row[colOf[j]] = v;
|
||||
}
|
||||
if (!file)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "PLY file ended after " << i << " of " << m_vertexCount << " vertices");
|
||||
values.release();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlyDecoder::parseBody(std::ifstream &file,
|
||||
std::vector<Point3f>& points, std::vector<Point3f>& normals,
|
||||
std::vector<Point3f>& rgb, std::vector<Point3f>& texCoords,
|
||||
|
||||
@@ -39,6 +39,8 @@ public:
|
||||
std::vector<Point3f>& texCoords, int& nTexCoords,
|
||||
std::vector<std::vector<int32_t>>& indices, int flags) CV_OVERRIDE;
|
||||
|
||||
bool readNamedProperties(const std::vector<std::string>& names, Mat& values);
|
||||
|
||||
protected:
|
||||
bool parseHeader(std::ifstream &file, int& nTexCoords);
|
||||
void parseBody(std::ifstream &file,
|
||||
|
||||
49
modules/ptcloud/src/io_splat.cpp
Normal file
49
modules/ptcloud/src/io_splat.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
// Copyright (C) 2026, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#include "precomp.hpp"
|
||||
|
||||
#include "io_splat.hpp"
|
||||
#include "splat.hpp"
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
namespace cv {
|
||||
|
||||
bool readSplatFile(const std::string& filename, Mat& splats)
|
||||
{
|
||||
splats.release();
|
||||
|
||||
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||
if (!file)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Failed to open '" << filename << "'");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::streamoff size = file.tellg();
|
||||
if (size <= 0 || size % (int)splat::PACKED_STRIDE != 0)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "'" << filename << "' is not a .splat file: size " << size
|
||||
<< " is not a positive multiple of " << (int)splat::PACKED_STRIDE);
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seekg(0);
|
||||
std::vector<uchar> buf((size_t)size);
|
||||
if (!file.read((char*)buf.data(), size))
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Failed to read '" << filename << "'");
|
||||
return false;
|
||||
}
|
||||
|
||||
decodeGaussianSplatsPacked(buf, splats);
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace cv */
|
||||
20
modules/ptcloud/src/io_splat.hpp
Normal file
20
modules/ptcloud/src/io_splat.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
// Copyright (C) 2026, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#ifndef _CODERS_SPLAT_H_
|
||||
#define _CODERS_SPLAT_H_
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace cv {
|
||||
|
||||
// Headerless, no point cloud attributes, so it stays outside BasePointCloudDecoder.
|
||||
bool readSplatFile(const std::string& filename, Mat& splats);
|
||||
|
||||
} /* namespace cv */
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "io_base.hpp"
|
||||
#include "io_obj.hpp"
|
||||
#include "io_ply.hpp"
|
||||
#include "io_splat.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "opencv2/core/utils/filesystem.private.hpp"
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
@@ -83,6 +84,53 @@ void loadPointCloud(const String &filename, OutputArray vertices, OutputArray no
|
||||
#endif
|
||||
}
|
||||
|
||||
void loadGaussianSplats(const String &filename, OutputArray splats,
|
||||
const std::vector<String> &ply_properties)
|
||||
{
|
||||
splats.release();
|
||||
|
||||
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
|
||||
String file_ext = getExtension(filename);
|
||||
Mat decoded;
|
||||
|
||||
if (file_ext == "splat" || file_ext == "SPLAT")
|
||||
{
|
||||
if (!readSplatFile(filename, decoded))
|
||||
return;
|
||||
}
|
||||
else if (file_ext == "ply" || file_ext == "PLY")
|
||||
{
|
||||
PlyDecoder decoder;
|
||||
decoder.setSource(filename);
|
||||
|
||||
const std::vector<String> names =
|
||||
ply_properties.empty() ? getGaussianSplatPlyPropNames() : ply_properties;
|
||||
|
||||
Mat raw;
|
||||
if (!decoder.readNamedProperties(names, raw))
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "'" << filename << "' is not a 3D Gaussian Splatting PLY file");
|
||||
return;
|
||||
}
|
||||
|
||||
decodeGaussianSplats(raw, decoded);
|
||||
}
|
||||
else
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Gaussian splats can only be loaded from PLY or SPLAT files, got '" << file_ext << "'");
|
||||
return;
|
||||
}
|
||||
|
||||
splats.move(decoded);
|
||||
|
||||
#else // OPENCV_HAVE_FILESYSTEM_SUPPORT
|
||||
CV_UNUSED(filename);
|
||||
CV_UNUSED(splats);
|
||||
CV_UNUSED(ply_properties);
|
||||
CV_LOG_WARNING(NULL, "File system support is disabled in this OpenCV build!");
|
||||
#endif
|
||||
}
|
||||
|
||||
void savePointCloud(const String &filename, InputArray vertices, InputArray normals, InputArray rgb)
|
||||
{
|
||||
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
|
||||
|
||||
118
modules/ptcloud/src/splat.cpp
Normal file
118
modules/ptcloud/src/splat.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
// Copyright (C) 2026, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#include "precomp.hpp"
|
||||
|
||||
#include "splat.hpp"
|
||||
|
||||
namespace cv {
|
||||
|
||||
std::vector<String> getGaussianSplatPlyPropNames()
|
||||
{
|
||||
return {
|
||||
"x", "y", "z",
|
||||
"f_dc_0", "f_dc_1", "f_dc_2",
|
||||
"opacity",
|
||||
"scale_0", "scale_1", "scale_2",
|
||||
"rot_0", "rot_1", "rot_2", "rot_3"
|
||||
};
|
||||
}
|
||||
|
||||
static void storeCovariance(float* d, const Matx33f& cov)
|
||||
{
|
||||
d[GAUSSIAN_SPLAT_COV + 0] = cov(0, 0);
|
||||
d[GAUSSIAN_SPLAT_COV + 1] = cov(0, 1);
|
||||
d[GAUSSIAN_SPLAT_COV + 2] = cov(0, 2);
|
||||
d[GAUSSIAN_SPLAT_COV + 3] = cov(1, 1);
|
||||
d[GAUSSIAN_SPLAT_COV + 4] = cov(1, 2);
|
||||
d[GAUSSIAN_SPLAT_COV + 5] = cov(2, 2);
|
||||
}
|
||||
|
||||
void decodeGaussianSplats(InputArray attributes, OutputArray splats)
|
||||
{
|
||||
CV_TRACE_FUNCTION();
|
||||
|
||||
Mat raw = attributes.getMat();
|
||||
CV_Assert(raw.dims == 2 && raw.channels() == 1);
|
||||
CV_Assert(raw.type() == CV_32F && raw.cols == splat::RAW_STRIDE);
|
||||
|
||||
splats.create(raw.rows, GAUSSIAN_SPLAT_STRIDE, CV_32F);
|
||||
Mat dst = splats.getMat();
|
||||
|
||||
parallel_for_(Range(0, raw.rows), [&](const Range& range)
|
||||
{
|
||||
for (int i = range.start; i < range.end; i++)
|
||||
{
|
||||
const float* s = raw.ptr<float>(i);
|
||||
float* d = dst.ptr<float>(i);
|
||||
|
||||
for (int k = 0; k < 3; k++)
|
||||
d[GAUSSIAN_SPLAT_POS + k] = s[splat::RAW_OFS_POS + k];
|
||||
|
||||
Vec3f scale(std::exp(s[splat::RAW_OFS_SCALE + 0]),
|
||||
std::exp(s[splat::RAW_OFS_SCALE + 1]),
|
||||
std::exp(s[splat::RAW_OFS_SCALE + 2]));
|
||||
storeCovariance(d, splat::covariance(scale,
|
||||
Vec4f(s[splat::RAW_OFS_ROT + 0], s[splat::RAW_OFS_ROT + 1],
|
||||
s[splat::RAW_OFS_ROT + 2], s[splat::RAW_OFS_ROT + 3])));
|
||||
|
||||
for (int k = 0; k < 3; k++)
|
||||
d[GAUSSIAN_SPLAT_RGB + k] = splat::shDcToColor(s[splat::RAW_OFS_DC + k]);
|
||||
|
||||
d[GAUSSIAN_SPLAT_ALPHA] = splat::sigmoid(s[splat::RAW_OFS_OPACITY]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void decodeGaussianSplatsPacked(InputArray buf, OutputArray splats)
|
||||
{
|
||||
CV_TRACE_FUNCTION();
|
||||
|
||||
Mat raw = buf.getMat();
|
||||
CV_Assert(raw.depth() == CV_8U && raw.isContinuous());
|
||||
|
||||
const size_t stride = (size_t)splat::PACKED_STRIDE;
|
||||
const size_t bytes = raw.total() * raw.elemSize();
|
||||
CV_Assert(bytes > 0 && bytes % stride == 0);
|
||||
|
||||
const uchar* data = raw.ptr<uchar>();
|
||||
const int n = (int)(bytes / stride);
|
||||
|
||||
splats.create(n, GAUSSIAN_SPLAT_STRIDE, CV_32F);
|
||||
Mat dst = splats.getMat();
|
||||
|
||||
// Values arrive already activated, so only the covariance is built.
|
||||
parallel_for_(Range(0, n), [&](const Range& range)
|
||||
{
|
||||
for (int i = range.start; i < range.end; i++)
|
||||
{
|
||||
const uchar* s = data + (size_t)i * splat::PACKED_STRIDE;
|
||||
float* d = dst.ptr<float>(i);
|
||||
|
||||
Vec3f pos, scale;
|
||||
memcpy(pos.val, s + splat::PACKED_OFS_POS, sizeof(pos.val));
|
||||
memcpy(scale.val, s + splat::PACKED_OFS_SCALE, sizeof(scale.val));
|
||||
|
||||
for (int k = 0; k < 3; k++)
|
||||
d[GAUSSIAN_SPLAT_POS + k] = pos[k];
|
||||
|
||||
Vec4f rot;
|
||||
for (int k = 0; k < 4; k++)
|
||||
rot[k] = (s[splat::PACKED_OFS_ROT + k] - 128.f) / 128.f;
|
||||
if (rot.dot(rot) < 1e-12f)
|
||||
rot = Vec4f(1.f, 0.f, 0.f, 0.f);
|
||||
|
||||
storeCovariance(d, splat::covariance(scale, rot));
|
||||
|
||||
for (int k = 0; k < 3; k++)
|
||||
d[GAUSSIAN_SPLAT_RGB + k] = s[splat::PACKED_OFS_RGBA + k] / 255.f;
|
||||
|
||||
d[GAUSSIAN_SPLAT_ALPHA] = s[splat::PACKED_OFS_RGBA + 3] / 255.f;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace cv
|
||||
100
modules/ptcloud/src/splat.hpp
Normal file
100
modules/ptcloud/src/splat.hpp
Normal file
@@ -0,0 +1,100 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
// Copyright (C) 2026, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#ifndef _CODERS_SPLAT_INTERNAL_H_
|
||||
#define _CODERS_SPLAT_INTERNAL_H_
|
||||
|
||||
#include "precomp.hpp"
|
||||
|
||||
#include <opencv2/core/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace cv {
|
||||
|
||||
// 3D Gaussian Splatting internals shared by the loader, the decoders and the renderer.
|
||||
// Deliberately free of OpenGL so it stays testable on headless builds.
|
||||
namespace splat {
|
||||
|
||||
enum
|
||||
{
|
||||
// Column order of getGaussianSplatPlyPropNames(), which decodeGaussianSplats() reads.
|
||||
RAW_STRIDE = 14,
|
||||
RAW_OFS_POS = 0,
|
||||
RAW_OFS_DC = 3,
|
||||
RAW_OFS_OPACITY = 6,
|
||||
RAW_OFS_SCALE = 7,
|
||||
RAW_OFS_ROT = 10,
|
||||
|
||||
// Byte layout of a ".splat" record, which decodeGaussianSplatsPacked() reads.
|
||||
PACKED_STRIDE = 32,
|
||||
PACKED_OFS_POS = 0,
|
||||
PACKED_OFS_SCALE = 12,
|
||||
PACKED_OFS_RGBA = 24,
|
||||
PACKED_OFS_ROT = 28
|
||||
};
|
||||
|
||||
inline float sigmoid(float x)
|
||||
{
|
||||
return 1.0f / (1.0f + std::exp(-x));
|
||||
}
|
||||
|
||||
inline float shDcToColor(float dc)
|
||||
{
|
||||
return std::min(1.0f, std::max(0.0f, 0.5f + 0.28209479177387814f * dc));
|
||||
}
|
||||
|
||||
// Sigma = R S S^T R^T, symmetric positive semi-definite for any rotation and scale.
|
||||
inline Matx33f covariance(const Vec3f& scale, const Vec4f& rot)
|
||||
{
|
||||
Matx33f rs = Quatf(rot[0], rot[1], rot[2], rot[3]).toRotMat3x3(QUAT_ASSUME_NOT_UNIT);
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
rs(i, j) *= scale[j];
|
||||
return rs * rs.t();
|
||||
}
|
||||
|
||||
// Column of a model-view matrix that gives camera-space z of a point, for the row vector
|
||||
// convention the viz3d shaders use: z = p * depth_col.xyz + depth_col.w.
|
||||
inline Vec4f depthColumn(const Matx44f& mv)
|
||||
{
|
||||
return Vec4f(mv(0, 2), mv(1, 2), mv(2, 2), mv(3, 2));
|
||||
}
|
||||
|
||||
inline float viewDepth(const Vec4f& depth_col, const float* p)
|
||||
{
|
||||
return p[0] * depth_col[0] + p[1] * depth_col[1] + p[2] * depth_col[2] + depth_col[3];
|
||||
}
|
||||
|
||||
// Back to front order of the rows of a Nx3 position matrix, as alpha blending needs.
|
||||
// Sorts by camera-space depth, the order the reference 3DGS rasterizer composites in,
|
||||
// so trained scenes blend the way they were optimized.
|
||||
inline void sortByDepth(const Mat& pos, const Vec4f& depth_col, std::vector<int>& order)
|
||||
{
|
||||
CV_Assert(pos.type() == CV_32F && pos.cols == 3);
|
||||
|
||||
const int n = pos.rows;
|
||||
Mat key(1, n, CV_32F);
|
||||
float* k = key.ptr<float>();
|
||||
|
||||
parallel_for_(Range(0, n), [&](const Range& range)
|
||||
{
|
||||
for (int i = range.start; i < range.end; i++)
|
||||
k[i] = viewDepth(depth_col, pos.ptr<float>(i));
|
||||
});
|
||||
|
||||
order.resize(n);
|
||||
Mat idx(1, n, CV_32S, order.data());
|
||||
sortIdx(key, idx, SORT_EVERY_ROW | SORT_DESCENDING);
|
||||
}
|
||||
|
||||
} // namespace splat
|
||||
} // namespace cv
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "../precomp.hpp"
|
||||
#include "viz3d_private.hpp"
|
||||
#include "grid_ticks.hpp"
|
||||
#include "../splat.hpp"
|
||||
#include "opencv2/core/utils/logger.hpp"
|
||||
#include "opencv2/imgproc.hpp"
|
||||
|
||||
@@ -543,6 +544,21 @@ void showPoints(const String& win_name, const String& obj_name, InputArray point
|
||||
#endif
|
||||
}
|
||||
|
||||
void showGaussianSplats(const String& win_name, const String& obj_name, InputArray splats)
|
||||
{
|
||||
CV_TRACE_FUNCTION();
|
||||
#ifndef HAVE_OPENGL
|
||||
CV_UNUSED(win_name);
|
||||
CV_UNUSED(obj_name);
|
||||
CV_UNUSED(splats);
|
||||
CV_Error(cv::Error::OpenGlNotSupported, "The library is compiled without OpenGL support");
|
||||
#else
|
||||
Window* win = getWindow(win_name);
|
||||
win->set(obj_name, makePtr<GaussianSplats>(splats));
|
||||
updateWindow(win_name);
|
||||
#endif
|
||||
}
|
||||
|
||||
void showRGBD(const String& win_name, const String& obj_name, InputArray img, const Matx33f& intrinsics, float scale)
|
||||
{
|
||||
CV_TRACE_FUNCTION();
|
||||
@@ -664,6 +680,7 @@ View::View()
|
||||
this->up = { 0.0f, 1.0f, 0.0f };
|
||||
|
||||
this->aspect = 1.0f;
|
||||
this->viewport = Size(1, 1);
|
||||
this->setPerspective(1.3f, 0.1f, 2000.0f);
|
||||
this->lookAt(this->origin, { 0.0f, 1.0f, 0.0f });
|
||||
}
|
||||
@@ -677,6 +694,11 @@ void View::setAspect(float aspect_)
|
||||
}
|
||||
}
|
||||
|
||||
void View::setViewport(Size viewport_)
|
||||
{
|
||||
this->viewport = viewport_;
|
||||
}
|
||||
|
||||
void View::setPerspective(float fov_, float z_near_, float z_far_)
|
||||
{
|
||||
this->fov = fov_;
|
||||
@@ -972,9 +994,11 @@ void Window::draw()
|
||||
Rect rect = getWindowImageRect(this->name);
|
||||
float aspect = static_cast<float>(rect.width) / static_cast<float>(rect.height);
|
||||
this->view.setAspect(aspect);
|
||||
this->view.setViewport(rect.size());
|
||||
|
||||
ogl::enable(ogl::DEPTH_TEST);
|
||||
ogl::clearColor(this->sky_color * 255.0f);
|
||||
ogl::clearDepth(1.0f);
|
||||
|
||||
if (this->grid)
|
||||
{
|
||||
@@ -988,6 +1012,23 @@ void Window::draw()
|
||||
}
|
||||
|
||||
for (auto& obj : this->objects)
|
||||
if (!obj.second->isTransparent())
|
||||
obj.second->draw(this->view, this->sun);
|
||||
|
||||
std::vector<std::pair<float, Object*>> transparent;
|
||||
for (auto& obj : this->objects)
|
||||
{
|
||||
if (obj.second->isTransparent())
|
||||
{
|
||||
const Vec3f c = obj.second->getCenter();
|
||||
const Vec4f depth_col = splat::depthColumn(obj.second->getModel() * this->view.getView());
|
||||
transparent.emplace_back(splat::viewDepth(depth_col, c.val), obj.second.get());
|
||||
}
|
||||
}
|
||||
std::stable_sort(transparent.begin(), transparent.end(),
|
||||
[](const std::pair<float, Object*>& a, const std::pair<float, Object*>& b)
|
||||
{ return a.first > b.first; });
|
||||
for (auto& obj : transparent)
|
||||
obj.second->draw(this->view, this->sun);
|
||||
}
|
||||
|
||||
@@ -1534,6 +1575,255 @@ void PointCloud::setShader(ogl::Program program_)
|
||||
this->proj_loc = this->program.getUniformLocation("proj");
|
||||
}
|
||||
|
||||
GaussianSplats::GaussianSplats(InputArray splats_)
|
||||
{
|
||||
CV_Assert(splats_.channels() == 1 && splats_.dims() == 2 && splats_.size().width == GAUSSIAN_SPLAT_STRIDE);
|
||||
CV_Assert(splats_.depth() == CV_32F);
|
||||
CV_Assert(splats_.size().height > 0);
|
||||
|
||||
Mat src = splats_.getMat();
|
||||
this->count = src.rows;
|
||||
|
||||
// Only the positions are kept on the CPU, for the depth sort.
|
||||
this->pos = src.colRange(0, 3).clone();
|
||||
Scalar mean_pos = mean(this->pos.reshape(3));
|
||||
this->center = Vec3f(static_cast<float>(mean_pos[0]), static_cast<float>(mean_pos[1]),
|
||||
static_cast<float>(mean_pos[2]));
|
||||
|
||||
Mat packed(this->count * 4, 1, CV_32FC4, Scalar::all(0.0));
|
||||
Vec4f* texel = packed.ptr<Vec4f>(0);
|
||||
for (int i = 0; i < this->count; i++)
|
||||
{
|
||||
const float* s = src.ptr<float>(i);
|
||||
texel[i * 4 + 0] = Vec4f(s[0], s[1], s[2], s[GAUSSIAN_SPLAT_ALPHA]);
|
||||
texel[i * 4 + 1] = Vec4f(s[GAUSSIAN_SPLAT_COV + 0], s[GAUSSIAN_SPLAT_COV + 1], s[GAUSSIAN_SPLAT_COV + 2], 0.0f);
|
||||
texel[i * 4 + 2] = Vec4f(s[GAUSSIAN_SPLAT_COV + 3], s[GAUSSIAN_SPLAT_COV + 4], s[GAUSSIAN_SPLAT_COV + 5], 0.0f);
|
||||
texel[i * 4 + 3] = Vec4f(s[GAUSSIAN_SPLAT_RGB + 0], s[GAUSSIAN_SPLAT_RGB + 1], s[GAUSSIAN_SPLAT_RGB + 2], 0.0f);
|
||||
}
|
||||
this->data.copyFrom(packed, ogl::Buffer::TEXTURE_BUFFER);
|
||||
this->data_tex.create(this->data, ogl::TextureBuffer::RGBA32F);
|
||||
|
||||
this->order_cpu.resize(this->count);
|
||||
for (int i = 0; i < this->count; i++)
|
||||
this->order_cpu[i] = i;
|
||||
this->order.copyFrom(Mat(this->count, 1, CV_32S, this->order_cpu.data()), ogl::Buffer::TEXTURE_BUFFER);
|
||||
this->order_tex.create(this->order, ogl::TextureBuffer::R32I);
|
||||
|
||||
float quad_data[] = {
|
||||
-1.0f, -1.0f,
|
||||
+1.0f, -1.0f,
|
||||
+1.0f, +1.0f,
|
||||
-1.0f, +1.0f,
|
||||
};
|
||||
this->quad.copyFrom(Mat(4, 2, CV_32F, quad_data), ogl::Buffer::ARRAY_BUFFER);
|
||||
|
||||
int quad_index_data[] = { 0, 1, 2, 2, 3, 0 };
|
||||
this->quad_indices.copyFrom(Mat(2, 3, CV_32S, quad_index_data), ogl::Buffer::ELEMENT_ARRAY_BUFFER);
|
||||
|
||||
this->va.create({
|
||||
{
|
||||
this->quad,
|
||||
2 * sizeof(float), 0,
|
||||
2, ogl::Attribute::FLOAT,
|
||||
false, false,
|
||||
0
|
||||
}
|
||||
});
|
||||
|
||||
this->sorted = false;
|
||||
this->last_mv = Matx44f::eye();
|
||||
}
|
||||
|
||||
void GaussianSplats::reorder(const Matx44f& mv)
|
||||
{
|
||||
splat::sortByDepth(this->pos, splat::depthColumn(mv), this->order_cpu);
|
||||
this->order.copyFrom(Mat(this->count, 1, CV_32S, this->order_cpu.data()), ogl::Buffer::TEXTURE_BUFFER);
|
||||
}
|
||||
|
||||
void GaussianSplats::draw(const View& view, const Light& light)
|
||||
{
|
||||
CV_UNUSED(light);
|
||||
|
||||
Matx44f model_ = this->getModel();
|
||||
|
||||
// Depth depends on the view direction as well as the camera position, so the sort is
|
||||
// keyed on the whole model-view matrix, the same one the shader projects with.
|
||||
Matx44f mv = model_ * view.getView();
|
||||
if (!this->sorted || norm(mv - this->last_mv) > 0.0)
|
||||
{
|
||||
this->reorder(mv);
|
||||
this->last_mv = mv;
|
||||
this->sorted = true;
|
||||
}
|
||||
|
||||
this->program.bind();
|
||||
this->va.bind();
|
||||
|
||||
Matx44f proj = view.getProj();
|
||||
Size vp = view.getViewport();
|
||||
|
||||
ogl::Program::setUniformMat4x4(this->model_loc, model_);
|
||||
ogl::Program::setUniformMat4x4(this->view_loc, view.getView());
|
||||
ogl::Program::setUniformMat4x4(this->proj_loc, proj);
|
||||
|
||||
ogl::Program::setUniformVec2(this->focal_loc, Vec2f(0.5f * vp.width * proj(0, 0),
|
||||
0.5f * vp.height * proj(1, 1)));
|
||||
ogl::Program::setUniformVec2(this->viewport_loc, Vec2f(static_cast<float>(vp.width),
|
||||
static_cast<float>(vp.height)));
|
||||
ogl::Program::setUniform1f(this->z_near_loc, view.getZNear());
|
||||
|
||||
// Unit 0 is bound last so the active texture unit is left where other objects expect it.
|
||||
this->order_tex.bind(1);
|
||||
ogl::Program::setUniform1i(this->order_loc, 1);
|
||||
this->data_tex.bind(0);
|
||||
ogl::Program::setUniform1i(this->data_loc, 0);
|
||||
|
||||
ogl::enable(ogl::BLEND);
|
||||
ogl::blendFunc(ogl::BLEND_SRC_ALPHA, ogl::BLEND_ONE_MINUS_SRC_ALPHA);
|
||||
ogl::depthMask(false);
|
||||
|
||||
this->quad_indices.bind(ogl::Buffer::ELEMENT_ARRAY_BUFFER);
|
||||
ogl::drawElementsInstanced(0, 6, ogl::UNSIGNED_INT, ogl::TRIANGLES, this->count);
|
||||
|
||||
ogl::depthMask(true);
|
||||
ogl::disable(ogl::BLEND);
|
||||
}
|
||||
|
||||
String GaussianSplats::getShaderName()
|
||||
{
|
||||
return "splats";
|
||||
}
|
||||
|
||||
ogl::Program GaussianSplats::buildShader()
|
||||
{
|
||||
auto vs = ogl::Shader(R"(
|
||||
#version 330 core
|
||||
|
||||
layout (location = 0) in vec2 quad;
|
||||
|
||||
out vec3 frag_color;
|
||||
out vec2 frag_offset;
|
||||
out float frag_alpha;
|
||||
|
||||
uniform mat4 model;
|
||||
uniform mat4 view;
|
||||
uniform mat4 proj;
|
||||
uniform vec2 focal;
|
||||
uniform vec2 viewport;
|
||||
uniform float z_near;
|
||||
uniform samplerBuffer splat_data;
|
||||
uniform isamplerBuffer splat_order;
|
||||
|
||||
const float CUTOFF = 3.0;
|
||||
const float LOWPASS = 0.3;
|
||||
const float MAX_EXTENT = 0.75;
|
||||
|
||||
void main() {
|
||||
int id = texelFetch(splat_order, gl_InstanceID).r;
|
||||
|
||||
vec4 d0 = texelFetch(splat_data, id * 4 + 0);
|
||||
vec4 d1 = texelFetch(splat_data, id * 4 + 1);
|
||||
vec4 d2 = texelFetch(splat_data, id * 4 + 2);
|
||||
vec4 d3 = texelFetch(splat_data, id * 4 + 3);
|
||||
|
||||
mat4 mv = model * view;
|
||||
vec4 cam = vec4(d0.xyz, 1.0) * mv;
|
||||
|
||||
frag_color = d3.rgb;
|
||||
frag_alpha = d0.w;
|
||||
frag_offset = quad * CUTOFF;
|
||||
|
||||
if (cam.z <= z_near) {
|
||||
gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
mat3 sigma = mat3(d1.x, d1.y, d1.z,
|
||||
d1.y, d2.x, d2.y,
|
||||
d1.z, d2.y, d2.z);
|
||||
|
||||
mat3 xf = mat3(mv);
|
||||
mat3 sigma_cam = transpose(xf) * sigma * xf;
|
||||
|
||||
float z2 = cam.z * cam.z;
|
||||
mat3x2 j = mat3x2(focal.x / cam.z, 0.0,
|
||||
0.0, focal.y / cam.z,
|
||||
-focal.x * cam.x / z2, -focal.y * cam.y / z2);
|
||||
|
||||
mat2 cov = j * sigma_cam * transpose(j);
|
||||
|
||||
float det_raw = determinant(cov);
|
||||
cov[0][0] += LOWPASS;
|
||||
cov[1][1] += LOWPASS;
|
||||
frag_alpha *= sqrt(max(0.0, det_raw / max(determinant(cov), 1e-9)));
|
||||
|
||||
float a = cov[0][0];
|
||||
float b = cov[0][1];
|
||||
float c = cov[1][1];
|
||||
float mid = 0.5 * (a + c);
|
||||
float disc = sqrt(max(0.0, mid * mid - (a * c - b * b)));
|
||||
float l1 = mid + disc;
|
||||
float l2 = max(mid - disc, 0.0);
|
||||
|
||||
float r1 = CUTOFF * sqrt(l1);
|
||||
float r2 = CUTOFF * sqrt(l2);
|
||||
|
||||
if (l1 <= 0.0 || r1 > MAX_EXTENT * max(viewport.x, viewport.y)) {
|
||||
gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 e1;
|
||||
if (abs(b) < 1e-9)
|
||||
e1 = (a >= c) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
||||
else
|
||||
e1 = normalize(vec2(b, l1 - a));
|
||||
vec2 e2 = vec2(-e1.y, e1.x);
|
||||
|
||||
vec2 offset = quad.x * r1 * e1 + quad.y * r2 * e2;
|
||||
|
||||
vec4 clip = cam * proj;
|
||||
clip.xy += offset * 2.0 / viewport * clip.w;
|
||||
gl_Position = clip;
|
||||
}
|
||||
)", ogl::Shader::VERTEX);
|
||||
|
||||
auto fs = ogl::Shader(R"(
|
||||
#version 330 core
|
||||
|
||||
in vec3 frag_color;
|
||||
in vec2 frag_offset;
|
||||
in float frag_alpha;
|
||||
|
||||
out vec4 color;
|
||||
|
||||
// Below one 8-bit level the contribution is invisible.
|
||||
const float MIN_ALPHA = 1.0 / 255.0;
|
||||
|
||||
void main() {
|
||||
float alpha = frag_alpha * exp(-0.5 * dot(frag_offset, frag_offset));
|
||||
if (alpha < MIN_ALPHA)
|
||||
discard;
|
||||
color = vec4(frag_color, alpha);
|
||||
}
|
||||
)", ogl::Shader::FRAGMENT);
|
||||
|
||||
return ogl::Program(vs, fs);
|
||||
}
|
||||
|
||||
void GaussianSplats::setShader(ogl::Program program_)
|
||||
{
|
||||
this->program = program_;
|
||||
this->model_loc = this->program.getUniformLocation("model");
|
||||
this->view_loc = this->program.getUniformLocation("view");
|
||||
this->proj_loc = this->program.getUniformLocation("proj");
|
||||
this->focal_loc = this->program.getUniformLocation("focal");
|
||||
this->viewport_loc = this->program.getUniformLocation("viewport");
|
||||
this->z_near_loc = this->program.getUniformLocation("z_near");
|
||||
this->data_loc = this->program.getUniformLocation("splat_data");
|
||||
this->order_loc = this->program.getUniformLocation("splat_order");
|
||||
}
|
||||
|
||||
#endif // HAVE_OPENGL
|
||||
|
||||
} // namespace viz3d
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
View();
|
||||
|
||||
void setAspect(float aspect);
|
||||
void setViewport(Size viewport);
|
||||
void setPerspective(float fov, float z_near, float z_far);
|
||||
|
||||
void rotate(float dx, float dy); // Rotates the camera using mouse input
|
||||
@@ -37,6 +38,8 @@ public:
|
||||
inline float getDistance() const { return this->distance; }
|
||||
inline Matx44f getView() const { return this->view; }
|
||||
inline Matx44f getProj() const { return this->proj; }
|
||||
inline Size getViewport() const { return this->viewport; }
|
||||
inline float getZNear() const { return this->z_near; }
|
||||
|
||||
private:
|
||||
void lookAt(const Vec3f& point, const Vec3f& up);
|
||||
@@ -45,6 +48,7 @@ private:
|
||||
float fov;
|
||||
float z_near;
|
||||
float z_far;
|
||||
Size viewport;
|
||||
|
||||
Matx44f proj;
|
||||
Matx44f view;
|
||||
@@ -79,6 +83,11 @@ public:
|
||||
virtual ogl::Program buildShader() = 0;
|
||||
virtual void setShader(ogl::Program program) = 0;
|
||||
|
||||
// Alpha-blended objects must be drawn after all opaque ones. See Window::draw.
|
||||
virtual bool isTransparent() const { return false; }
|
||||
// Point in object space by which transparent objects are ordered against each other.
|
||||
virtual Vec3f getCenter() const { return Vec3f::all(0.0f); }
|
||||
|
||||
inline Matx44f getModel() const { return this->model; }
|
||||
|
||||
private:
|
||||
@@ -203,6 +212,50 @@ private:
|
||||
int proj_loc;
|
||||
};
|
||||
|
||||
// 3D Gaussian Splatting object.
|
||||
class GaussianSplats : public Object
|
||||
{
|
||||
public:
|
||||
GaussianSplats(InputArray splats);
|
||||
|
||||
virtual void draw(const View& view, const Light& light) override;
|
||||
|
||||
virtual String getShaderName() override;
|
||||
virtual ogl::Program buildShader() override;
|
||||
virtual void setShader(ogl::Program program) override;
|
||||
|
||||
virtual bool isTransparent() const override { return true; }
|
||||
virtual Vec3f getCenter() const override { return this->center; }
|
||||
|
||||
private:
|
||||
void reorder(const Matx44f& mv);
|
||||
|
||||
ogl::Program program;
|
||||
ogl::VertexArray va;
|
||||
ogl::Buffer quad;
|
||||
ogl::Buffer quad_indices;
|
||||
ogl::Buffer data;
|
||||
ogl::Buffer order;
|
||||
ogl::TextureBuffer data_tex;
|
||||
ogl::TextureBuffer order_tex;
|
||||
|
||||
Mat pos;
|
||||
std::vector<int> order_cpu;
|
||||
Vec3f center;
|
||||
Matx44f last_mv;
|
||||
bool sorted;
|
||||
int count;
|
||||
|
||||
int model_loc;
|
||||
int view_loc;
|
||||
int proj_loc;
|
||||
int focal_loc;
|
||||
int viewport_loc;
|
||||
int z_near_loc;
|
||||
int data_loc;
|
||||
int order_loc;
|
||||
};
|
||||
|
||||
}} // namespace cv::viz3d
|
||||
|
||||
#endif // HAVE_OPENGL
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
#include "test_precomp.hpp"
|
||||
#include <opencv2/highgui.hpp> // updateWindow / destroyWindow
|
||||
#include "../src/viz3d/grid_ticks.hpp" // GL-free grid spacing helper
|
||||
#include "../src/splat.hpp" // GL-free 3DGS decode/sort helpers
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
namespace opencv_test { namespace {
|
||||
|
||||
@@ -28,6 +32,295 @@ TEST(Viz3D, grid_tick_step)
|
||||
}
|
||||
}
|
||||
|
||||
// Identity rotation + unit scale must give the identity covariance. GL-free.
|
||||
TEST(Splat, covariance_identity)
|
||||
{
|
||||
Matx33f cov = splat::covariance(Vec3f(1.f, 1.f, 1.f), Vec4f(1.f, 0.f, 0.f, 0.f));
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
EXPECT_NEAR(cov(i, j), (i == j) ? 1.f : 0.f, 1e-5f) << "at " << i << "," << j;
|
||||
}
|
||||
|
||||
// Axis-aligned scales must land on the diagonal as squares, and a non-unit
|
||||
// quaternion must still be treated as a rotation (i.e. normalized).
|
||||
TEST(Splat, covariance_scale_and_normalization)
|
||||
{
|
||||
Matx33f cov = splat::covariance(Vec3f(2.f, 3.f, 4.f), Vec4f(1.f, 0.f, 0.f, 0.f));
|
||||
EXPECT_NEAR(cov(0, 0), 4.f, 1e-4f);
|
||||
EXPECT_NEAR(cov(1, 1), 9.f, 1e-4f);
|
||||
EXPECT_NEAR(cov(2, 2), 16.f, 1e-4f);
|
||||
EXPECT_NEAR(cov(0, 1), 0.f, 1e-4f);
|
||||
|
||||
Matx33f scaled = splat::covariance(Vec3f(1.f, 1.f, 1.f), Vec4f(7.f, 0.f, 0.f, 0.f));
|
||||
for (int i = 0; i < 3; i++)
|
||||
EXPECT_NEAR(scaled(i, i), 1.f, 1e-5f) << "unnormalized quaternion changed the shape";
|
||||
}
|
||||
|
||||
// A 90 degree rotation about Z must swap the X and Y variances.
|
||||
TEST(Splat, covariance_rotation)
|
||||
{
|
||||
const float s = std::sqrt(0.5f);
|
||||
Matx33f cov = splat::covariance(Vec3f(2.f, 3.f, 4.f), Vec4f(s, 0.f, 0.f, s));
|
||||
EXPECT_NEAR(cov(0, 0), 9.f, 1e-3f);
|
||||
EXPECT_NEAR(cov(1, 1), 4.f, 1e-3f);
|
||||
EXPECT_NEAR(cov(2, 2), 16.f, 1e-3f);
|
||||
EXPECT_NEAR(cov(0, 1), 0.f, 1e-3f);
|
||||
}
|
||||
|
||||
// Covariance must stay symmetric for arbitrary inputs.
|
||||
TEST(Splat, covariance_symmetric)
|
||||
{
|
||||
RNG rng(0x5eed);
|
||||
for (int t = 0; t < 64; t++)
|
||||
{
|
||||
Vec3f scale(rng.uniform(0.01f, 3.f), rng.uniform(0.01f, 3.f), rng.uniform(0.01f, 3.f));
|
||||
Vec4f q(rng.uniform(-1.f, 1.f), rng.uniform(-1.f, 1.f),
|
||||
rng.uniform(-1.f, 1.f), rng.uniform(-1.f, 1.f));
|
||||
if (cv::norm(q) < 1e-3)
|
||||
continue;
|
||||
Matx33f cov = splat::covariance(scale, q);
|
||||
EXPECT_NEAR(cov(0, 1), cov(1, 0), 1e-4f);
|
||||
EXPECT_NEAR(cov(0, 2), cov(2, 0), 1e-4f);
|
||||
EXPECT_NEAR(cov(1, 2), cov(2, 1), 1e-4f);
|
||||
EXPECT_GT(cov(0, 0), 0.f);
|
||||
EXPECT_GT(cov(1, 1), 0.f);
|
||||
EXPECT_GT(cov(2, 2), 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Splat, decode_activations)
|
||||
{
|
||||
Mat raw = Mat::zeros(1, splat::RAW_STRIDE, CV_32F);
|
||||
float* r = raw.ptr<float>(0);
|
||||
r[0] = 1.f; r[1] = 2.f; r[2] = 3.f; // position
|
||||
r[10] = 1.f; // identity quaternion, zero scale, zero opacity, zero f_dc
|
||||
|
||||
Mat splats;
|
||||
decodeGaussianSplats(raw, splats);
|
||||
|
||||
ASSERT_EQ(splats.rows, 1);
|
||||
ASSERT_EQ(splats.cols, (int)GAUSSIAN_SPLAT_STRIDE);
|
||||
|
||||
const float* d = splats.ptr<float>(0);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_POS + 0], 1.f, 1e-6f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_POS + 1], 2.f, 1e-6f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_POS + 2], 3.f, 1e-6f);
|
||||
|
||||
// exp(0) == 1 on every axis, so the covariance is the identity
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 0], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 3], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 5], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 1], 0.f, 1e-5f);
|
||||
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_RGB + 0], 0.5f, 1e-5f); // 0.5 + C0 * 0
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_ALPHA], 0.5f, 1e-5f); // sigmoid(0)
|
||||
}
|
||||
|
||||
// Sorting must run strictly far to near, which is what alpha blending requires.
|
||||
TEST(Splat, sort_far_to_near)
|
||||
{
|
||||
Mat pos = Mat::zeros(4, 3, CV_32F);
|
||||
const float xs[] = { 1.f, 8.f, 4.f, 2.f };
|
||||
for (int i = 0; i < 4; i++)
|
||||
pos.ptr<float>(i)[0] = xs[i];
|
||||
|
||||
// Depth taken along +x.
|
||||
std::vector<int> order;
|
||||
splat::sortByDepth(pos, Vec4f(1.f, 0.f, 0.f, 0.f), order);
|
||||
|
||||
ASSERT_EQ(order.size(), (size_t)4);
|
||||
EXPECT_EQ(order[0], 1);
|
||||
EXPECT_EQ(order[1], 2);
|
||||
EXPECT_EQ(order[2], 3);
|
||||
EXPECT_EQ(order[3], 0);
|
||||
|
||||
for (size_t i = 1; i < order.size(); i++)
|
||||
{
|
||||
float prev = pos.ptr<float>(order[i - 1])[0];
|
||||
float cur = pos.ptr<float>(order[i])[0];
|
||||
EXPECT_GE(prev, cur);
|
||||
}
|
||||
}
|
||||
|
||||
// A point far off to the side is further from the camera but shallower in depth.
|
||||
// Sorting must follow depth, as the reference 3DGS rasterizer does, not distance.
|
||||
TEST(Splat, sort_by_view_depth_not_distance)
|
||||
{
|
||||
const float data[] = {
|
||||
0.f, 0.f, 5.f, // on axis: distance 5, depth 5
|
||||
10.f, 0.f, 4.f // off axis: distance ~10.8, depth 4
|
||||
};
|
||||
Mat pos(2, 3, CV_32F, (void*)data);
|
||||
|
||||
std::vector<int> order;
|
||||
splat::sortByDepth(pos, splat::depthColumn(Matx44f::eye()), order);
|
||||
|
||||
ASSERT_EQ(order.size(), (size_t)2);
|
||||
EXPECT_EQ(order[0], 0);
|
||||
EXPECT_EQ(order[1], 1);
|
||||
}
|
||||
|
||||
// depthColumn must reproduce the camera-space z the shaders get from vec4(p, 1) * model * view.
|
||||
TEST(Splat, depth_column_matches_row_vector_transform)
|
||||
{
|
||||
// A camera at (1, 2, -3) looking along +z: view rows as View::lookAt builds them.
|
||||
const Vec3f eye(1.f, 2.f, -3.f);
|
||||
const Vec3f s(1.f, 0.f, 0.f), u(0.f, 1.f, 0.f), f(0.f, 0.f, 1.f);
|
||||
Matx44f view(s(0), u(0), f(0), 0.f,
|
||||
s(1), u(1), f(1), 0.f,
|
||||
s(2), u(2), f(2), 0.f,
|
||||
-s.dot(eye), -u.dot(eye), -f.dot(eye), 1.f);
|
||||
|
||||
// A model that moves the object by (0, 0, 2).
|
||||
Matx44f model = Matx44f::eye();
|
||||
model(3, 2) = 2.f;
|
||||
|
||||
const Matx44f mv = model * view;
|
||||
const float p[] = { 4.f, -1.f, 7.f };
|
||||
Matx<float, 1, 4> cam = Matx<float, 1, 4>(p[0], p[1], p[2], 1.f) * mv;
|
||||
|
||||
// Object z 7, moved to 9 in the world, 12 in front of a camera at z = -3.
|
||||
EXPECT_NEAR(cam(0, 2), 12.f, 1e-5f);
|
||||
EXPECT_NEAR(splat::viewDepth(splat::depthColumn(mv), p), cam(0, 2), 1e-5f);
|
||||
}
|
||||
|
||||
// Writes a minimal 3DGS PLY. Properties are deliberately out of order and an
|
||||
// unrequested one is included, to prove the reader maps by name and skips extras.
|
||||
static void writeSplatPly(const std::string& path)
|
||||
{
|
||||
std::ofstream f(path);
|
||||
f << "ply\n"
|
||||
<< "format ascii 1.0\n"
|
||||
<< "element vertex 2\n"
|
||||
<< "property float x\n"
|
||||
<< "property float y\n"
|
||||
<< "property float z\n"
|
||||
<< "property float f_rest_0\n"
|
||||
<< "property float opacity\n"
|
||||
<< "property float scale_0\n"
|
||||
<< "property float scale_1\n"
|
||||
<< "property float scale_2\n"
|
||||
<< "property float rot_0\n"
|
||||
<< "property float rot_1\n"
|
||||
<< "property float rot_2\n"
|
||||
<< "property float rot_3\n"
|
||||
<< "property float f_dc_0\n"
|
||||
<< "property float f_dc_1\n"
|
||||
<< "property float f_dc_2\n"
|
||||
<< "end_header\n"
|
||||
<< "1 2 3 99 0 0 0 0 1 0 0 0 0 0 0\n"
|
||||
<< "4 5 6 99 0 0 0 0 1 0 0 0 0 0 0\n";
|
||||
}
|
||||
|
||||
TEST(Splat, load_ply)
|
||||
{
|
||||
std::string path = tempfile("splats.ply");
|
||||
writeSplatPly(path);
|
||||
|
||||
Mat splats;
|
||||
loadGaussianSplats(path, splats);
|
||||
remove(path.c_str());
|
||||
|
||||
ASSERT_FALSE(splats.empty());
|
||||
ASSERT_EQ(splats.rows, 2);
|
||||
ASSERT_EQ(splats.cols, (int)GAUSSIAN_SPLAT_STRIDE);
|
||||
|
||||
EXPECT_NEAR(splats.ptr<float>(0)[0], 1.f, 1e-6f);
|
||||
EXPECT_NEAR(splats.ptr<float>(0)[2], 3.f, 1e-6f);
|
||||
EXPECT_NEAR(splats.ptr<float>(1)[0], 4.f, 1e-6f);
|
||||
EXPECT_NEAR(splats.ptr<float>(1)[2], 6.f, 1e-6f);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
const float* d = splats.ptr<float>(i);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_ALPHA], 0.5f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 0], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_RGB + 0], 0.5f, 1e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
// A PLY without the 3DGS attributes must fail cleanly rather than return garbage.
|
||||
TEST(Splat, load_ply_rejects_plain_cloud)
|
||||
{
|
||||
std::string path = tempfile("plain.ply");
|
||||
{
|
||||
std::ofstream f(path);
|
||||
f << "ply\nformat ascii 1.0\nelement vertex 1\n"
|
||||
<< "property float x\nproperty float y\nproperty float z\n"
|
||||
<< "end_header\n0 0 0\n";
|
||||
}
|
||||
|
||||
Mat splats;
|
||||
loadGaussianSplats(path, splats);
|
||||
remove(path.c_str());
|
||||
|
||||
EXPECT_TRUE(splats.empty());
|
||||
}
|
||||
|
||||
static void writeSplatFile(const std::string& path, int n)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
const float pos[3] = { 1.f + 3 * i, 2.f + 3 * i, 3.f + 3 * i };
|
||||
const float scale[3] = { 1.f, 1.f, 1.f };
|
||||
const uchar rgba[4] = { 128, 128, 128, 128 };
|
||||
const uchar rot[4] = { 255, 128, 128, 128 };
|
||||
f.write((const char*)pos, sizeof(pos));
|
||||
f.write((const char*)scale, sizeof(scale));
|
||||
f.write((const char*)rgba, sizeof(rgba));
|
||||
f.write((const char*)rot, sizeof(rot));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Splat, load_splat)
|
||||
{
|
||||
std::string path = tempfile("splats.splat");
|
||||
writeSplatFile(path, 2);
|
||||
|
||||
Mat splats;
|
||||
loadGaussianSplats(path, splats);
|
||||
remove(path.c_str());
|
||||
|
||||
ASSERT_FALSE(splats.empty());
|
||||
ASSERT_EQ(splats.rows, 2);
|
||||
ASSERT_EQ(splats.cols, (int)GAUSSIAN_SPLAT_STRIDE);
|
||||
|
||||
EXPECT_NEAR(splats.ptr<float>(0)[0], 1.f, 1e-6f);
|
||||
EXPECT_NEAR(splats.ptr<float>(0)[2], 3.f, 1e-6f);
|
||||
EXPECT_NEAR(splats.ptr<float>(1)[0], 4.f, 1e-6f);
|
||||
EXPECT_NEAR(splats.ptr<float>(1)[2], 6.f, 1e-6f);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
const float* d = splats.ptr<float>(i);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_ALPHA], 128.f / 255.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_RGB + 0], 128.f / 255.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 0], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 3], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 5], 1.f, 1e-5f);
|
||||
EXPECT_NEAR(d[GAUSSIAN_SPLAT_COV + 1], 0.f, 1e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
// A record is 32 bytes, so anything else is truncated or not a splat file at all.
|
||||
TEST(Splat, load_splat_rejects_partial_record)
|
||||
{
|
||||
std::string path = tempfile("partial.splat");
|
||||
writeSplatFile(path, 1);
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary | std::ios::app);
|
||||
const uchar tail[8] = { 0 };
|
||||
f.write((const char*)tail, sizeof(tail));
|
||||
}
|
||||
|
||||
Mat splats;
|
||||
loadGaussianSplats(path, splats);
|
||||
remove(path.c_str());
|
||||
|
||||
EXPECT_TRUE(splats.empty());
|
||||
}
|
||||
|
||||
// viz3d needs a GL context; skip when none is available (headless CI).
|
||||
static bool viz3dAvailable()
|
||||
{
|
||||
@@ -74,4 +367,70 @@ TEST(Viz3D, render_scene_smoke)
|
||||
destroyAllWindows();
|
||||
}
|
||||
|
||||
// Exercises the splat path: TextureBuffer, instanced draw, blend/depthMask state,
|
||||
// and that splats compose with opaque objects in the same window.
|
||||
TEST(Viz3D, render_splats_smoke)
|
||||
{
|
||||
if (!viz3dAvailable())
|
||||
throw cvtest::SkipTestException("viz3d/OpenGL not available (no GL context)");
|
||||
|
||||
const String w = "viz3d_splat_test";
|
||||
|
||||
Mat raw(64, splat::RAW_STRIDE, CV_32F, Scalar::all(0.0));
|
||||
RNG rng(0x5B1A7C);
|
||||
for (int i = 0; i < raw.rows; i++)
|
||||
{
|
||||
float* r = raw.ptr<float>(i);
|
||||
r[0] = rng.uniform(-2.f, 2.f);
|
||||
r[1] = rng.uniform(-2.f, 2.f);
|
||||
r[2] = rng.uniform(-2.f, 2.f);
|
||||
r[6] = 1.0f; // opacity -> sigmoid
|
||||
r[7] = r[8] = r[9] = -2.0f; // small scales
|
||||
r[10] = 1.0f; // identity quaternion
|
||||
}
|
||||
|
||||
Mat splats;
|
||||
decodeGaussianSplats(raw, splats);
|
||||
|
||||
EXPECT_NO_THROW(viz3d::showGaussianSplats(w, "splats", splats));
|
||||
|
||||
// An opaque object in the same window: splats must be drawn after it.
|
||||
Mat pts(16, 6, CV_32F);
|
||||
randu(pts, 0.0f, 1.0f);
|
||||
EXPECT_NO_THROW(viz3d::showPoints(w, "pts", pts));
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
EXPECT_NO_THROW(updateWindow(w));
|
||||
|
||||
// Changing the model matrix must invalidate the depth order, since the sort
|
||||
// runs in object space.
|
||||
EXPECT_NO_THROW(viz3d::setObjectPosition(w, "splats", Vec3f(1.0f, 0.0f, 0.0f)));
|
||||
EXPECT_NO_THROW(updateWindow(w));
|
||||
|
||||
EXPECT_NO_THROW(viz3d::destroyObject(w, "splats"));
|
||||
destroyAllWindows();
|
||||
}
|
||||
|
||||
// A single centered splat must not throw and must survive repeated redraws.
|
||||
TEST(Viz3D, render_single_splat)
|
||||
{
|
||||
if (!viz3dAvailable())
|
||||
throw cvtest::SkipTestException("viz3d/OpenGL not available (no GL context)");
|
||||
|
||||
const String w = "viz3d_single_splat";
|
||||
|
||||
Mat raw = Mat::zeros(1, splat::RAW_STRIDE, CV_32F);
|
||||
raw.ptr<float>(0)[6] = 2.0f;
|
||||
raw.ptr<float>(0)[10] = 1.0f;
|
||||
|
||||
Mat splats;
|
||||
decodeGaussianSplats(raw, splats);
|
||||
|
||||
EXPECT_NO_THROW(viz3d::showGaussianSplats(w, "one", splats));
|
||||
for (int i = 0; i < 4; ++i)
|
||||
EXPECT_NO_THROW(updateWindow(w));
|
||||
|
||||
destroyAllWindows();
|
||||
}
|
||||
|
||||
}} // namespace
|
||||
|
||||
184
samples/ptcloud/splats.cpp
Normal file
184
samples/ptcloud/splats.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
// Copyright (C) 2026, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/highgui.hpp>
|
||||
#include <opencv2/ptcloud.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <iostream>
|
||||
|
||||
using namespace cv;
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const char* keys =
|
||||
"{ help h | | print this message }"
|
||||
"{ @splats | | path to a trained 3D Gaussian Splatting .ply or .splat file }"
|
||||
"{ points | false | also show the splat centers as an opaque point cloud }"
|
||||
"{ raw | false | keep the file's world coordinates instead of recentering }"
|
||||
"{ flip | true | rotate 180 deg about X, for the COLMAP Y-down convention }"
|
||||
"{ fit | 8.0 | half-extent to scale the scene to when recentering }"
|
||||
"{ size | 0.5 | splat sigma as a multiple of point spacing, for plain point clouds }";
|
||||
|
||||
CommandLineParser parser(argc, argv, keys);
|
||||
parser.about("Renders a 3D Gaussian Splatting scene with cv::viz3d::showGaussianSplats.");
|
||||
|
||||
if (!parser.check())
|
||||
{
|
||||
parser.printErrors();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (parser.has("help") || !parser.has("@splats"))
|
||||
{
|
||||
parser.printMessage();
|
||||
std::cout << "\nA trained 3DGS scene is not bundled with OpenCV; pass your own, e.g.\n"
|
||||
<< " ./example_ptcloud_splats /path/to/point_cloud.ply\n"
|
||||
<< " ./example_ptcloud_splats /path/to/scene.splat\n"
|
||||
<< "A plain colored point cloud also works, rendered as isotropic splats:\n"
|
||||
<< " ./example_ptcloud_splats ../samples/data/anthidium-forcipatum.ply\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
const String filename = parser.get<String>("@splats");
|
||||
|
||||
Mat splats;
|
||||
loadGaussianSplats(filename, splats);
|
||||
const bool trained = !splats.empty();
|
||||
|
||||
if (!trained)
|
||||
{
|
||||
Mat verts, rgb;
|
||||
loadPointCloud(filename, verts, noArray(), rgb);
|
||||
if (verts.empty())
|
||||
{
|
||||
std::cerr << "Failed to load '" << filename << "' as splats or as a point cloud.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
verts = verts.reshape(1, verts.rows);
|
||||
if (!rgb.empty())
|
||||
rgb = rgb.reshape(1, rgb.rows);
|
||||
|
||||
Vec3f plo(FLT_MAX, FLT_MAX, FLT_MAX), phi(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
||||
for (int i = 0; i < verts.rows; i++)
|
||||
for (int k = 0; k < 3; k++)
|
||||
{
|
||||
plo[k] = std::min(plo[k], verts.at<float>(i, k));
|
||||
phi[k] = std::max(phi[k], verts.at<float>(i, k));
|
||||
}
|
||||
|
||||
// Scanned clouds sample a surface, not a volume, so spacing goes as sqrt(area/N).
|
||||
Vec3f ext;
|
||||
for (int k = 0; k < 3; k++)
|
||||
ext[k] = std::max(1e-6f, phi[k] - plo[k]);
|
||||
double area = 2.0 * (ext[0] * ext[1] + ext[1] * ext[2] + ext[2] * ext[0]);
|
||||
float sigma = parser.get<float>("size")
|
||||
* (float)std::sqrt(area / std::max(1, verts.rows));
|
||||
|
||||
splats = Mat::zeros(verts.rows, 13, CV_32F);
|
||||
for (int i = 0; i < verts.rows; i++)
|
||||
{
|
||||
float* d = splats.ptr<float>(i);
|
||||
for (int k = 0; k < 3; k++)
|
||||
d[k] = verts.at<float>(i, k);
|
||||
d[3] = d[6] = d[8] = sigma * sigma;
|
||||
for (int k = 0; k < 3; k++)
|
||||
d[9 + k] = rgb.empty() ? 0.8f : rgb.at<float>(i, k);
|
||||
d[12] = 1.0f;
|
||||
}
|
||||
|
||||
std::cout << "'" << filename << "' is not a trained 3DGS file; loaded it as a point cloud "
|
||||
<< "and synthesized isotropic splats with sigma " << sigma
|
||||
<< " (tune with --size)" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "Loaded " << splats.rows << " splats from " << filename << std::endl;
|
||||
|
||||
if (trained && parser.get<bool>("flip"))
|
||||
{
|
||||
for (int i = 0; i < splats.rows; i++)
|
||||
{
|
||||
float* p = splats.ptr<float>(i);
|
||||
p[1] = -p[1];
|
||||
p[2] = -p[2];
|
||||
p[4] = -p[4];
|
||||
p[5] = -p[5];
|
||||
}
|
||||
std::cout << "rotated 180 degrees about X for the COLMAP Y-down convention"
|
||||
<< " (pass --flip=false to disable)" << std::endl;
|
||||
}
|
||||
|
||||
Mat loM, hiM;
|
||||
reduce(splats.colRange(0, 3), loM, 0, REDUCE_MIN);
|
||||
reduce(splats.colRange(0, 3), hiM, 0, REDUCE_MAX);
|
||||
Vec3f lo(loM.ptr<float>()), hi(hiM.ptr<float>());
|
||||
|
||||
std::cout << "bounds min [" << lo[0] << ", " << lo[1] << ", " << lo[2] << "]"
|
||||
<< " max [" << hi[0] << ", " << hi[1] << ", " << hi[2] << "]" << std::endl;
|
||||
|
||||
if (!parser.get<bool>("raw"))
|
||||
{
|
||||
std::vector<float> axis(splats.rows);
|
||||
Vec3f med(0.f, 0.f, 0.f);
|
||||
for (int k = 0; k < 3; k++)
|
||||
{
|
||||
for (int i = 0; i < splats.rows; i++)
|
||||
axis[i] = splats.ptr<float>(i)[k];
|
||||
std::nth_element(axis.begin(), axis.begin() + axis.size() / 2, axis.end());
|
||||
med[k] = axis[axis.size() / 2];
|
||||
}
|
||||
|
||||
std::vector<float> rad(splats.rows);
|
||||
for (int i = 0; i < splats.rows; i++)
|
||||
{
|
||||
const float* p = splats.ptr<float>(i);
|
||||
rad[i] = std::max(std::max(std::abs(p[0] - med[0]), std::abs(p[1] - med[1])),
|
||||
std::abs(p[2] - med[2]));
|
||||
}
|
||||
size_t q = (size_t)(rad.size() * 0.9);
|
||||
std::nth_element(rad.begin(), rad.begin() + q, rad.end());
|
||||
float extent = rad[q];
|
||||
|
||||
std::cout << "median [" << med[0] << ", " << med[1] << ", " << med[2]
|
||||
<< "] p90 half-extent " << extent << std::endl;
|
||||
|
||||
float scale = (extent > 0.0f) ? parser.get<float>("fit") / extent : 1.0f;
|
||||
|
||||
for (int i = 0; i < splats.rows; i++)
|
||||
{
|
||||
float* p = splats.ptr<float>(i);
|
||||
for (int k = 0; k < 3; k++)
|
||||
p[k] = (p[k] - med[k]) * scale;
|
||||
for (int k = 0; k < 6; k++)
|
||||
p[3 + k] *= scale * scale;
|
||||
}
|
||||
|
||||
std::cout << "recentered on the origin and scaled by " << scale
|
||||
<< " (pass --raw to disable)" << std::endl;
|
||||
}
|
||||
|
||||
viz3d::setPerspective("splats", 1.3f, 0.01f, 1000.0f);
|
||||
viz3d::showGaussianSplats("splats", "scene", splats);
|
||||
|
||||
if (parser.get<bool>("points"))
|
||||
{
|
||||
Mat centers(splats.rows, 6, CV_32F);
|
||||
splats.colRange(0, 3).copyTo(centers.colRange(0, 3));
|
||||
splats.colRange(9, 12).copyTo(centers.colRange(3, 6));
|
||||
viz3d::showPoints("splats", "centers", centers);
|
||||
}
|
||||
|
||||
std::cout << "Drag with the left mouse button to orbit, right to pan, wheel to zoom. "
|
||||
<< "Press Esc to quit." << std::endl;
|
||||
|
||||
while (waitKey(16) != 27)
|
||||
;
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user