Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

pcl::isFinite type implementation #2664

Closed
greenbrettmichael opened this issue Nov 30, 2018 · 17 comments · Fixed by #3402
Closed

pcl::isFinite type implementation #2664

greenbrettmichael opened this issue Nov 30, 2018 · 17 comments · Fixed by #3402
Labels
changelog: enhancement Meta-information for changelog generation module: common

Comments

@greenbrettmichael
Copy link
Contributor

The downside is that pcl::isFinite is not actually implemented for a couple of types. It requires a type-trait/meta-programming magic, in order to avoid having to define the method explicitly to all newly added types.

Originally posted by @SergioRAgostinho in #2518 (comment)

@greenbrettmichael
Copy link
Contributor Author

greenbrettmichael commented Nov 30, 2018

I am very close. Here is my current code snippet (I am focusing on registered points at the moment).

I am having a hard time with pcl::detail::FiniteMapper::operator () (). It seems that boost mpl is having difficulties in unwrapping the iterator for the point mpl vector. I have tried an alternate method using boost::mpl::for_each with the same trouble

#include <pcl/point_types.h>
#include <pcl/point_traits.h>
#include <pcl/for_each_type.h>
#include <type_traits>


namespace pcl
{
  namespace detail
  {
    /** maps values of point to finite bool */
    template<typename PointT>
    struct FiniteMapper
    {
      FiniteMapper(const PointT& pt, bool& isInfinite_)
        : pt(pt), isInfinite_(isInfinite_)
      {
      }

      template<typename Key> inline void
        operator () ()
      {
        typedef typename pcl::traits::datatype<PointT, Key>::type keyType;
        keyType checkValue;
        memcpy(reinterpret_cast<uint8_t*>(&checkValue),
          reinterpret_cast<const uint8_t*>(&pt) + pcl::traits::offset<PointT, Key>::value,
          sizeof(keyType));
        isInfinite_ |= std::isfinite(checkValue);
      }

      const PointT& pt;
      bool& isInfinite_;
    };
  }

  /** Tests if a value is finite
    * param[in] val class to check if finite
    * return true if finite, false otherwise
    */
  template<typename PointT>
  inline bool isFinite(const PointT& pt)
  {
    typedef typename pcl::traits::fieldList<PointT>::type pointList;
    bool isInfinite = false;
    detail::FiniteMapper<PointT> mapper(pt, isInfinite);
    for_each_type <pointList>(mapper);
    return !isInfinite;
  }
}

Maintainer Edit: sintax highlight

@SergioRAgostinho SergioRAgostinho added module: common changelog: enhancement Meta-information for changelog generation labels Nov 30, 2018
@SergioRAgostinho
Copy link
Member

I modified your snippet and this is working for me now.

#include <pcl/point_types.h>
#include <pcl/point_traits.h>
#include <pcl/for_each_type.h>

namespace pcl
{
  namespace traits
  {
    template<typename PointT>
    struct IsFinite
    {

      IsFinite (const PointT& pt, bool& value) : pt (pt), value (value)
      {
        typedef typename traits::fieldList<PointT>::type FieldList;
        for_each_type <FieldList>(*this);
      }

      template<typename Key> inline void
      operator () ()
      {
        typedef typename datatype<PointT, Key>::type KeyType;
        const KeyType* value = (const KeyType*)((const char*)&pt + offset<PointT, Key>::value);
        this->value &= std::isfinite(*value);
      }

      const PointT& pt;
      bool& value;
    };
  }


  /** Tests if a value is finite
    * param[in] val class to check if finite
    * return true if finite, false otherwise
    */
  template<typename PointT>
  bool newIsFinite(const PointT& pt)
  {
    bool is_finite = true;
    traits::IsFinite<PointT> mapper (pt, is_finite);
    return is_finite;
  }
}


int
main()
{

  pcl::PointXYZRGBNormal p;
  std::cout << p << '\n' << pcl::newIsFinite (p) << std::endl;

  p.normal_z = std::numeric_limits<float>::infinity();
  std::cout << p << '\n' << pcl::newIsFinite (p) << std::endl;

  p.normal_z = 0;
  p.y = std::numeric_limits<float>::quiet_NaN();
  std::cout << p << '\n' << pcl::newIsFinite (p) << std::endl;
  return 0;
}
$ ./test 
(0,0,0 - -1.70141e+38 - 0,0,0 - 0, 0, 0 - 0)
1
(0,0,0 - -1.70141e+38 - 0,0,inf - 0, 0, 0 - 0)
0
(0,nan,0 - -1.70141e+38 - 0,0,0 - 0, 0, 0 - 0)
0

@greenbrettmichael
Copy link
Contributor Author

greenbrettmichael commented Dec 1, 2018

I still get the same compilation error that the call to fpclassify is ambiguous. What is your environment? I am using MSVC 15.9.3 on Windows 10. I am wondering how I am having a problem with this given that copy_point.hpp uses the same mechanism.

I have attached the full output trace in case anyone recognizes this funny business.

5>C:\Program Files (x86)\Windows Kits\10\Include\10.0.17134.0\ucrt\corecrt_math.h(403): error C2668: 'fpclassify': ambiguous call to overloaded function
5>C:\Program Files (x86)\Windows Kits\10\Include\10.0.17134.0\ucrt\corecrt_math.h(300): note: could be 'int fpclassify(long double) throw()'
5>C:\Program Files (x86)\Windows Kits\10\Include\10.0.17134.0\ucrt\corecrt_math.h(295): note: or       'int fpclassify(double) throw()'
5>C:\Program Files (x86)\Windows Kits\10\Include\10.0.17134.0\ucrt\corecrt_math.h(290): note: or       'int fpclassify(float) throw()'
5>C:\Program Files (x86)\Windows Kits\10\Include\10.0.17134.0\ucrt\corecrt_math.h(403): note: while trying to match the argument list '(_Ty)'
5>        with
5>        [
5>            _Ty=KeyType
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/common/point_tests.h(71): note: see reference to function template instantiation 'bool isfinite<KeyType>(_Ty) throw()' being compiled
5>        with
5>        [
5>            _Ty=KeyType
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/for_each_type.h(79): note: see reference to function template instantiation 'void pcl::traits::IsFinite<PointT>::operator ()<arg>(void)' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/for_each_type.h(79): note: see reference to function template instantiation 'void pcl::traits::IsFinite<PointT>::operator ()<arg>(void)' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/for_each_type.h(86): note: see reference to function template instantiation 'void pcl::for_each_type_impl<false>::execute<iter,LastIterator,F>(F)' being compiled
5>        with
5>        [
5>            LastIterator=last,
5>            F=pcl::traits::IsFinite<pcl::PointXYZRGBA>
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/for_each_type.h(86): note: see reference to function template instantiation 'void pcl::for_each_type_impl<false>::execute<iter,LastIterator,F>(F)' being compiled
5>        with
5>        [
5>            LastIterator=last,
5>            F=pcl::traits::IsFinite<pcl::PointXYZRGBA>
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/for_each_type.h(86): note: see reference to function template instantiation 'void pcl::for_each_type_impl<false>::execute<iter,LastIterator,F>(F)' being compiled
5>        with
5>        [
5>            LastIterator=last,
5>            F=pcl::traits::IsFinite<pcl::PointXYZRGBA>
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/for_each_type.h(97): note: see reference to function template instantiation 'void pcl::for_each_type_impl<false>::execute<first,last,F>(F)' being compiled
5>        with
5>        [
5>            F=pcl::traits::IsFinite<pcl::PointXYZRGBA>
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/common/point_tests.h(63): note: see reference to function template instantiation 'void pcl::for_each_type<FieldList,pcl::traits::IsFinite<PointT>>(F)' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA,
5>            F=pcl::traits::IsFinite<pcl::PointXYZRGBA>
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/common/point_tests.h(60): note: while compiling class template member function 'pcl::traits::IsFinite<PointT>::IsFinite(const PointT &,bool &)'
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/common/point_tests.h(87): note: see reference to function template instantiation 'pcl::traits::IsFinite<PointT>::IsFinite(const PointT &,bool &)' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA
5>        ]
5>C:\Projects\pcl\pcl\common\include\pcl/common/point_tests.h(87): note: see reference to class template instantiation 'pcl::traits::IsFinite<PointT>' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA
5>        ]
5>C:\Projects\pcl\pcl\octree\include\pcl/octree/impl/octree_pointcloud.hpp(75): note: see reference to function template instantiation 'bool pcl::isFinite<_Ty>(const PointT &)' being compiled
5>        with
5>        [
5>            _Ty=pcl::PointXYZRGBA,
5>            PointT=pcl::PointXYZRGBA
5>        ]
5>C:\Projects\pcl\pcl\octree\include\pcl/octree/impl/octree_pointcloud.hpp(66): note: while compiling class template member function 'void pcl::octree::OctreePointCloud<PointT,LeafT,BranchT,OctreeT>::addPointsFromInputCloud(void)'
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA,
5>            LeafT=pcl::octree::OctreeContainerPointIndices,
5>            BranchT=pcl::octree::OctreeContainerEmpty,
5>            OctreeT=pcl::octree::Octree2BufBase<pcl::octree::OctreeContainerPointIndices,pcl::octree::OctreeContainerEmpty>
5>        ]
5>C:\Projects\pcl\pcl\io\include\pcl/compression/impl/octree_pointcloud_compression.hpp(67): note: see reference to function template instantiation 'void pcl::octree::OctreePointCloud<PointT,LeafT,BranchT,OctreeT>::addPointsFromInputCloud(void)' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA,
5>            LeafT=pcl::octree::OctreeContainerPointIndices,
5>            BranchT=pcl::octree::OctreeContainerEmpty,
5>            OctreeT=pcl::octree::Octree2BufBase<pcl::octree::OctreeContainerPointIndices,pcl::octree::OctreeContainerEmpty>
5>        ]
5>C:\Projects\pcl\pcl\io\include\pcl/compression/octree_pointcloud_compression.h(76): note: see reference to class template instantiation 'pcl::octree::OctreePointCloud<PointT,LeafT,BranchT,OctreeT>' being compiled
5>        with
5>        [
5>            PointT=pcl::PointXYZRGBA,
5>            LeafT=pcl::octree::OctreeContainerPointIndices,
5>            BranchT=pcl::octree::OctreeContainerEmpty,
5>            OctreeT=pcl::octree::Octree2BufBase<pcl::octree::OctreeContainerPointIndices,pcl::octree::OctreeContainerEmpty>
5>        ]
5>C:\Projects\pcl\pcl\io\src\compression.cpp(50): note: see reference to class template instantiation 'pcl::io::OctreePointCloudCompression<pcl::PointXYZRGBA,pcl::octree::OctreeContainerPointIndices,pcl::octree::OctreeContainerEmpty,pcl::octree::Octree2BufBase<LeafT,BranchT>>' being compiled
5>        with
5>        [
5>            LeafT=pcl::octree::OctreeContainerPointIndices,
5>            BranchT=pcl::octree::OctreeContainerEmpty
5>        ]

@SergioRAgostinho
Copy link
Member

SergioRAgostinho commented Dec 1, 2018

My env is
OS: macOS 10.13.6
Compiler: Apple LLVM version 10.0.0 (clang-1000.10.44.4)
PCL: master

Given the error, the fix here will likely be explicitly invoking the some function with the correct template instantiation i.e.

function<KeyType>()

I am just confused because the log is pointing to a couple of lines which in my version of the lib do not make sense. Specifically

template<> inline bool isFinite<pcl::PrincipalRadiiRSD> (const pcl::PrincipalRadiiRSD&) { return (true); }

I'm now trying to compile the full library replacing the original definitions with this new version. It worked ok.

#pragma once

#ifdef _MSC_VER
#include <Eigen/src/StlSupport/details.h>
#endif

#include <pcl/for_each_type.h>

namespace pcl
{
//   /** Tests if the 3D components of a point are all finite
//     * param[in] pt point to be tested
//     * return true if finite, false otherwise
//     */
//   template <typename PointT> inline bool
//   isFinite (const PointT &pt)
//   {
//     return (pcl_isfinite (pt.x) && pcl_isfinite (pt.y) && pcl_isfinite (pt.z));
//   }

// #ifdef _MSC_VER
//   template <typename PointT> inline bool
//   isFinite (const Eigen::internal::workaround_msvc_stl_support<PointT> &pt)
//   {
//     return isFinite<PointT> (static_cast<const PointT&> (pt));
//   }
// #endif

//   template<> inline bool isFinite<pcl::RGB> (const pcl::RGB&) { return (true); }
//   template<> inline bool isFinite<pcl::Label> (const pcl::Label&) { return (true); }
//   template<> inline bool isFinite<pcl::Axis> (const pcl::Axis&) { return (true); }
//   template<> inline bool isFinite<pcl::Intensity> (const pcl::Intensity&) { return (true); }
//   template<> inline bool isFinite<pcl::MomentInvariants> (const pcl::MomentInvariants&) { return (true); }
//   template<> inline bool isFinite<pcl::PrincipalRadiiRSD> (const pcl::PrincipalRadiiRSD&) { return (true); }
//   template<> inline bool isFinite<pcl::Boundary> (const pcl::Boundary&) { return (true); }
//   template<> inline bool isFinite<pcl::PrincipalCurvatures> (const pcl::PrincipalCurvatures&) { return (true); }
//   template<> inline bool isFinite<pcl::SHOT352> (const pcl::SHOT352&) { return (true); }
//   template<> inline bool isFinite<pcl::SHOT1344> (const pcl::SHOT1344&) { return (true); }
//   template<> inline bool isFinite<pcl::ReferenceFrame> (const pcl::ReferenceFrame&) { return (true); }
//   template<> inline bool isFinite<pcl::ShapeContext1980> (const pcl::ShapeContext1980&) { return (true); }
//   template<> inline bool isFinite<pcl::UniqueShapeContext1960> (const pcl::UniqueShapeContext1960&) { return (true); }
//   template<> inline bool isFinite<pcl::PFHSignature125> (const pcl::PFHSignature125&) { return (true); }
//   template<> inline bool isFinite<pcl::PFHRGBSignature250> (const pcl::PFHRGBSignature250&) { return (true); }
//   template<> inline bool isFinite<pcl::PPFSignature> (const pcl::PPFSignature&) { return (true); }
//   template<> inline bool isFinite<pcl::PPFRGBSignature> (const pcl::PPFRGBSignature&) { return (true); }
//   template<> inline bool isFinite<pcl::NormalBasedSignature12> (const pcl::NormalBasedSignature12&) { return (true); }
//   template<> inline bool isFinite<pcl::FPFHSignature33> (const pcl::FPFHSignature33&) { return (true); }
//   template<> inline bool isFinite<pcl::VFHSignature308> (const pcl::VFHSignature308&) { return (true); }
//   template<> inline bool isFinite<pcl::ESFSignature640> (const pcl::ESFSignature640&) { return (true); }
//   template<> inline bool isFinite<pcl::IntensityGradient> (const pcl::IntensityGradient&) { return (true); }
//   template<> inline bool isFinite<pcl::BRISKSignature512> (const pcl::BRISKSignature512&) { return (true); }

//   // specification for pcl::PointXY
//   template <> inline bool
//   isFinite<pcl::PointXY> (const pcl::PointXY &p)
//   {
//     return (pcl_isfinite (p.x) && pcl_isfinite (p.y));
//   }

//   // specification for pcl::BorderDescription
//   template <> inline bool
//   isFinite<pcl::BorderDescription> (const pcl::BorderDescription &p)
//   {
//     return (pcl_isfinite (p.x) && pcl_isfinite (p.y));
//   }

//   // specification for pcl::Normal
//   template <> inline bool
//   isFinite<pcl::Normal> (const pcl::Normal &n)
//   {
//     return (pcl_isfinite (n.normal_x) && pcl_isfinite (n.normal_y) && pcl_isfinite (n.normal_z));
//   }

  namespace traits
  {
    template<typename PointT>
    struct IsFinite
    {

      IsFinite (const PointT& pt, bool& value) : pt (pt), value (value)
      {
        typedef typename traits::fieldList<PointT>::type FieldList;
        for_each_type <FieldList>(*this);
      }

      template<typename Key> inline void
      operator () ()
      {
        typedef typename datatype<PointT, Key>::type KeyType;
        const KeyType* value = (const KeyType*)((const char*)&pt + offset<PointT, Key>::value);
        this->value &= pcl_isfinite(*value);
      }

      const PointT& pt;
      bool& value;
    };
  }


  /** Tests if a value is finite
    * param[in] val class to check if finite
    * return true if finite, false otherwise
    */
  template<typename PointT>
  bool isFinite(const PointT& pt)
  {
    bool is_finite = true;
    traits::IsFinite<PointT> mapper (pt, is_finite);
    return is_finite;
  }
}

@taketwo
Copy link
Member

taketwo commented Dec 3, 2018

@SergioRAgostinho your snippet looks reasonable to me, I was able to mentally compile it without errors :)

I think you should not comment out template specializations and the #ifdef block with MSVC workaround.

@SergioRAgostinho
Copy link
Member

I was able to mentally compile it without errors :)

I need to get that upgrade myself next time I go to maintenance. This snippet just get's the basics done.

I think you should not comment out template specializations and the #ifdef block with MSVC workaround.

Agreed.

Ideally I would like to completely ignore non floating point fields, to compose a FieldList which only has floating point fields. Basically as quick as it can get.

@taketwo
Copy link
Member

taketwo commented Dec 3, 2018

That's a good idea, then we won't need explicit instantiations and it will work for custom point types out of the box.

@SergioRAgostinho
Copy link
Member

SergioRAgostinho commented Dec 6, 2018

Second attempt

#include <pcl/point_types.h>
#include <pcl/point_traits.h>
#include <pcl/for_each_type.h>

namespace pcl
{
  namespace traits
  {
    template<typename PointT>
    struct IsFinite
    {

      IsFinite (const PointT& pt, bool& value) : pt (pt), value (value)
      {
        typedef typename traits::fieldList<PointT>::type FieldList;
        for_each_type<FieldList>(*this);
      }

      template<typename Key> inline
      typename boost::enable_if_c<boost::is_float<typename datatype<PointT, Key>::type>::value>::type
      operator () ()
      {
        std::cout << "iter " << typeid(typename asType<datatype<PointT, Key>::value >::type).name() << std::endl;
        typedef typename datatype<PointT, Key>::type KeyType;
        const KeyType* value = (const KeyType*)((const char*)&pt + offset<PointT, Key>::value);
        this->value &= std::isfinite(*value);
      }


      template<typename Key> inline
      typename boost::disable_if_c<boost::is_float<typename datatype<PointT, Key>::type>::value>::type
      operator () () {}

      const PointT& pt;
      bool& value;
    };
  }


  /** Tests if a value is finite
    * param[in] val class to check if finite
    * return true if finite, false otherwise
    */
  template<typename PointT>
  bool newIsFinite(const PointT& pt)
  {
    bool is_finite = true;
    traits::IsFinite<PointT> mapper (pt, is_finite);
    return is_finite;
  }
}


int
main()
{

  pcl::PointXYZRGBA p;
  std::cout << p << '\n' << pcl::newIsFinite (p) << std::endl;

  p.z = std::numeric_limits<float>::infinity();
  std::cout << p << '\n' << pcl::newIsFinite (p) << std::endl;

  p.z = 0;
  p.y = std::numeric_limits<float>::quiet_NaN();
  std::cout << p << '\n' << pcl::newIsFinite (p) << std::endl;
  return 0;
}

which outputs

$ ./test 
(0,0,0 - 0,0,0,255)
iter f
iter f
iter f
1
(0,0,inf - 0,0,0,255)
iter f
iter f
iter f
0
(0,nan,0 - 0,0,0,255)
iter f
iter f
iter f
0

This get's the job done but there's still that extra call to the empty function, which I assume is optimized out. The hurdle here is that this traits::fieldList<PointT> is populated by the field names (no surprise) and not by the actual types. This blocks the immediate use of boost::mpl::filter_view.

Edit: I just noticed all our histogram types are float arrays :') Basically we cannot get rid of the specializations.

@taketwo
Copy link
Member

taketwo commented Dec 6, 2018

We can always check the value of datatype<PointT, Key>::size. It will be more than one for arrays, in which case we can loop.

@SergioRAgostinho
Copy link
Member

The current proposal does not contemplate arrays, that's true. But my comment was not so much on the fact that they're arrays, but that they are histograms whose elements are floats instead of an integer type. Their specialization of isFinite returns true immediately and our generic call would just iterate through all components.

@taketwo
Copy link
Member

taketwo commented Dec 6, 2018

We can do whatever we want: iterate and check whether each value in the histogram is finite, or immediately output true.

But anyway, I think we have a more conceptual question to answer here. As it is, isFinite tests if x, y, and z components of a point are finite (if it has them). One exception is pcl::Normal point type, for which normal components are checked, but let's ignore this for now. So a more precise name for isFinite would probably be isXYZFinite.

Most (all?) algorithms that use this function actually mean to test only x, y, z. If we were to adopt the method we are discussing, this would make a big difference for them. All of a sudden, points with finite coordinates but, say, invalid curvature will be skipped. Besides, the runtime for points that have many float fields can increase dramatically.

With this in mind, I'd certainly avoid replacing isFinite with this new implementation. The fact that we can write a function to check finity of all float fields does not mean that we need such a function :)

@SergioRAgostinho
Copy link
Member

You're right. Now I feel like I went down the rabbit whole too quickly on this one. :) Oh well was still an interesting crash course into MPL. By the way...

https://boostorg.github.io/hana/

@taketwo
Copy link
Member

taketwo commented Dec 6, 2018

I've never got around to use this one. Once I even started to read intro/tutorial, but then realized that required standard and compiler versions are too high for my project and I can not lift them due to CUDA issues.

@kunaltyagi
Copy link
Member

kunaltyagi commented Oct 1, 2019

Kicking this conversation (since it was referred) alive. My rationale for a function is not having to repeat those checks again.

So a more precise name for isFinite would probably be isXYZFinite

Agreed.

How about simpler and more narrow focussed functions like isXYZFinite, isNormalFinite and isCurvatureFinite? It makes more sense from an algorithm perspective. Reading if (!isNormalFinite(point) || !isXYZFinite(point)) continue; is a tad more explanatory than if (!isFinite(point)).

Will be easier to write these functions too without Boost MPL using pcl::traits

Pro: less typing in code, not dependent on new future types in PCL
Con: more code

EDIT:

struct Point {
    int x,y,z;
    int normal_x, normal_y, normal_z;
};

template <class PointT>
using HasNormal = typename std::enable_if<traits::has_normal<PointT>::value, bool>::type;

template <class PointT, HasNormal<PointT> = true>
constexpr bool isNormalFinite(const PointT& point) noexcept
{
    return point.normal_x == point.normal_y;  // could even use MPL trick to compare
}

constexpr auto val = isNormalFinite(Point{});

@taketwo
Copy link
Member

taketwo commented Oct 1, 2019

Please have a look at the pcl::traits namespace:

namespace traits

The meta-functions from this namespace can be used to test the availability of particular fields in a point.

@kunaltyagi
Copy link
Member

required standard and compiler versions are too high for my project

Brigand might be the library for you

@taketwo
Copy link
Member

taketwo commented Oct 2, 2019

Thanks for the link, haven't heard of it. But too late anyway :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog: enhancement Meta-information for changelog generation module: common
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants