Merge pull request #29799 from lazerg:fix/issue-29798-dilate-bool

imgproc: accept CV_Bool input in morphological operations - #29799

### Pull Request Readiness Checklist

Fixes #29798

In 5.0 `cv::Mat_<bool>::depth()` returns `CV_Bool` instead of `CV_8U`, so the morphology filter factories in `morph.simd.hpp` no longer matched any branch and threw `Unsupported data type`. This worked in 4.x.

`CV_Bool` is a 1-byte 0/1 type, so it is dispatched through the same `uchar` min/max filters as `CV_8U`, and the default border value follows the `CV_8U` case (1 for erode, 0 for dilate). The OpenCL path bails out for `CV_Bool` and falls back to the CPU implementation, since `ocl::typeToStr` has no mapping for it.

Accepting `CV_Bool` also exposed a latent bug in `FilterEngine::init`: `borderElemSize` was computed from `CV_MAT_DEPTH(srcType) >= CV_32S`, which assumes the depth enum is ordered by element size. `CV_Bool` is 9, so a 1-byte type took the `sizeof(int)` branch and `borderElemSize` became 0, leaving `borderTab` empty and making every non-`BORDER_CONSTANT` erode/dilate read through a null pointer. It now keys off `CV_ELEM_SIZE1` instead. `CV_16F`/`CV_16BF` had the same latent miscomputation.

Added a test comparing `Mat_<bool>` against the equivalent `CV_8UC1` mask for erode, dilate, open and close over the three structuring element shapes and four border types, plus in-place and multi-iteration calls.

`MORPH_GRADIENT`/`TOPHAT`/`BLACKHAT` remain unsupported for `CV_Bool`: they subtract two masks, which has no meaning for a boolean type, and they throw rather than returning a wrong result.

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
- [x] 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:
Lazizbek Ergashev authored and GitHub committed 2026-10-23 10:56:49 +03:00
1 parent 7bd8de1f31
commit 7c3d04a044
4 files changed
+73 -10

No files matched your search

+6 -1
View File
@@ -145,7 +145,12 @@ void FilterEngine::init( const Ptr<BaseFilter>& _filter2D,
CV_Assert( 0 <= anchor.x && anchor.x < ksize.width &&
0 <= anchor.y && anchor.y < ksize.height );
borderElemSize = srcElemSize/(CV_MAT_DEPTH(srcType) >= CV_32S ? sizeof(int) : 1);
// borderTab keeps the source offsets of the border elements of one row. FilterEngine__proceed()
// reads them as int offsets when borderElemSize*sizeof(int) is the element size, and as byte
// offsets in all other cases. An element whose channel type is smaller than int uses the byte
// form, so borderElemSize is the full element size in bytes there.
borderElemSize = CV_ELEM_SIZE1(srcType) >= (int)sizeof(int) ?
srcElemSize/(int)sizeof(int) : srcElemSize;
int borderLength = std::max(ksize.width - 1, 1);
borderTab.resize(borderLength*borderElemSize);
+5 -3
View File
@@ -113,15 +113,16 @@ Ptr<FilterEngine> createMorphologyFilter(
borderValue == morphologyDefaultBorderValue() )
{
int depth = CV_MAT_DEPTH(type);
CV_Assert( depth == CV_8U || depth == CV_16U || depth == CV_16S ||
CV_Assert( depth == CV_8U || depth == CV_Bool || depth == CV_16U || depth == CV_16S ||
depth == CV_32F || depth == CV_64F );
if( op == MORPH_ERODE )
borderValue = Scalar::all( depth == CV_8U ? (double)UCHAR_MAX :
depth == CV_Bool ? 1. :
depth == CV_16U ? (double)USHRT_MAX :
depth == CV_16S ? (double)SHRT_MAX :
depth == CV_32F ? (double)FLT_MAX : DBL_MAX);
else
borderValue = Scalar::all( depth == CV_8U || depth == CV_16U ?
borderValue = Scalar::all( depth == CV_8U || depth == CV_Bool || depth == CV_16U ?
0. :
depth == CV_16S ? (double)SHRT_MIN :
depth == CV_32F ? (double)-FLT_MAX : -DBL_MAX);
@@ -797,7 +798,8 @@ static bool ocl_morphOp(InputArray _src, OutputArray _dst, InputArray _kernel,
Size ksize = !kernel.empty() ? kernel.size() : Size(3, 3), ssize = _src.size();
bool doubleSupport = dev.doubleFPConfig() > 0;
if ((depth == CV_64F && !doubleSupport) || borderType != BORDER_CONSTANT)
// ocl::typeToStr() has no mapping for CV_Bool, so fall back to the CPU path
if ((depth == CV_64F && !doubleSupport) || borderType != BORDER_CONSTANT || depth == CV_Bool)
return false;
bool haveExtraMat = !_extraMat.empty();
+6 -6
View File
@@ -724,7 +724,7 @@ Ptr<BaseRowFilter> getMorphologyRowFilter(int op, int type, int ksize, int ancho
CV_Assert( op == MORPH_ERODE || op == MORPH_DILATE );
if( op == MORPH_ERODE )
{
if( depth == CV_8U )
if( depth == CV_8U || depth == CV_Bool )
return makePtr<MorphRowFilter<MinOp<uchar>,
ErodeRowVec8u> >(ksize, anchor);
if( depth == CV_16U )
@@ -742,7 +742,7 @@ Ptr<BaseRowFilter> getMorphologyRowFilter(int op, int type, int ksize, int ancho
}
else
{
if( depth == CV_8U )
if( depth == CV_8U || depth == CV_Bool )
return makePtr<MorphRowFilter<MaxOp<uchar>,
DilateRowVec8u> >(ksize, anchor);
if( depth == CV_16U )
@@ -772,7 +772,7 @@ Ptr<BaseColumnFilter> getMorphologyColumnFilter(int op, int type, int ksize, int
CV_Assert( op == MORPH_ERODE || op == MORPH_DILATE );
if( op == MORPH_ERODE )
{
if( depth == CV_8U )
if( depth == CV_8U || depth == CV_Bool )
return makePtr<MorphColumnFilter<MinOp<uchar>,
ErodeColumnVec8u> >(ksize, anchor);
if( depth == CV_16U )
@@ -790,7 +790,7 @@ Ptr<BaseColumnFilter> getMorphologyColumnFilter(int op, int type, int ksize, int
}
else
{
if( depth == CV_8U )
if( depth == CV_8U || depth == CV_Bool )
return makePtr<MorphColumnFilter<MaxOp<uchar>,
DilateColumnVec8u> >(ksize, anchor);
if( depth == CV_16U )
@@ -819,7 +819,7 @@ Ptr<BaseFilter> getMorphologyFilter(int op, int type, const Mat& kernel, Point a
CV_Assert( op == MORPH_ERODE || op == MORPH_DILATE );
if( op == MORPH_ERODE )
{
if( depth == CV_8U )
if( depth == CV_8U || depth == CV_Bool )
return makePtr<MorphFilter<MinOp<uchar>, ErodeVec8u> >(kernel, anchor);
if( depth == CV_16U )
return makePtr<MorphFilter<MinOp<ushort>, ErodeVec16u> >(kernel, anchor);
@@ -832,7 +832,7 @@ Ptr<BaseFilter> getMorphologyFilter(int op, int type, const Mat& kernel, Point a
}
else
{
if( depth == CV_8U )
if( depth == CV_8U || depth == CV_Bool )
return makePtr<MorphFilter<MaxOp<uchar>, DilateVec8u> >(kernel, anchor);
if( depth == CV_16U )
return makePtr<MorphFilter<MaxOp<ushort>, DilateVec16u> >(kernel, anchor);
+56
View File
@@ -1071,6 +1071,62 @@ TEST(Imgproc, morphologyEx_small_input_22893)
ASSERT_EQ(0, cvtest::norm(result, gold, NORM_INF));
}
// See https://github.com/opencv/opencv/issues/29798
// cv::Mat_<bool>::depth() returns CV_Bool in 5.0, where it was CV_8U in 4.x, so
// morphological operations should keep accepting boolean masks as binary input.
typedef TestWithParam< tuple<int, int, int> > Imgproc_Morphology_Bool;
TEST_P(Imgproc_Morphology_Bool, bool_mask_29798)
{
const int op = get<0>(GetParam());
const int shape = get<1>(GetParam());
const int borderType = get<2>(GetParam());
Mat_<bool> mask(7, 9, false);
mask(1, 1) = true;
mask(2, 2) = true;
mask(2, 3) = true;
mask(3, 3) = true;
mask(5, 6) = true;
mask(5, 7) = true;
mask(6, 7) = true;
Mat_<uchar> ref;
mask.convertTo(ref, CV_8U);
ASSERT_EQ(mask.depth(), CV_Bool);
Mat kernel = getStructuringElement(shape, Size(3, 3));
Mat result_bool, result_ref, result_8u;
ASSERT_NO_THROW(morphologyEx(mask, result_bool, op, kernel, Point(-1, -1), 1, borderType));
morphologyEx(ref, result_ref, op, kernel, Point(-1, -1), 1, borderType);
EXPECT_EQ(result_bool.depth(), CV_Bool);
result_bool.convertTo(result_8u, CV_8U);
EXPECT_EQ(0, cvtest::norm(result_8u, result_ref, NORM_INF));
// The issue reports cv::dilate directly, so cover it in-place and with several iterations too.
Mat_<bool> inplace = mask.clone();
Mat_<uchar> inplace_ref = ref.clone();
ASSERT_NO_THROW(cv::dilate(inplace, inplace, kernel, Point(-1, -1), 2, borderType));
cv::erode(inplace, inplace, kernel, Point(-1, -1), 2, borderType);
cv::dilate(inplace_ref, inplace_ref, kernel, Point(-1, -1), 2, borderType);
cv::erode(inplace_ref, inplace_ref, kernel, Point(-1, -1), 2, borderType);
Mat inplace_8u;
inplace.convertTo(inplace_8u, CV_8U);
EXPECT_EQ(0, cvtest::norm(inplace_8u, inplace_ref, NORM_INF));
}
INSTANTIATE_TEST_CASE_P(/**/, Imgproc_Morphology_Bool,
Combine(
Values(MORPH_ERODE, MORPH_DILATE, MORPH_OPEN, MORPH_CLOSE),
Values(MORPH_RECT, MORPH_CROSS, MORPH_ELLIPSE),
Values(BORDER_CONSTANT, BORDER_REPLICATE, BORDER_REFLECT, BORDER_REFLECT_101)
)
);
TEST(Imgproc_sepFilter2D, identity)
{
std::vector<uint8_t> kernelX{0, 0, 0, 1, 0, 0, 0};