From be520d8fdf6757a97c50e5d79168edbd619760a7 Mon Sep 17 00:00:00 2001 From: Yu Changming Date: Tue, 15 Jul 2025 20:02:46 +0800 Subject: [PATCH 1/2] ximgproc: add hole filling for run-length encoded images --- .../ximgproc/run_length_morphology.hpp | 13 ++ .../ximgproc/src/run_length_morphology.cpp | 145 ++++++++++++++++++ .../test/test_run_length_morphology.cpp | 26 ++++ 3 files changed, 184 insertions(+) diff --git a/modules/ximgproc/include/opencv2/ximgproc/run_length_morphology.hpp b/modules/ximgproc/include/opencv2/ximgproc/run_length_morphology.hpp index 6cf2eb663c1..bf47ee2d04d 100644 --- a/modules/ximgproc/include/opencv2/ximgproc/run_length_morphology.hpp +++ b/modules/ximgproc/include/opencv2/ximgproc/run_length_morphology.hpp @@ -113,6 +113,19 @@ CV_EXPORTS void createRLEImage(const std::vector& runs, OutputArray CV_EXPORTS void morphologyEx(InputArray rlSrc, OutputArray rlDest, int op, InputArray rlKernel, bool bBoundaryOnForErosion = true, Point anchor = Point(0,0)); +/** +* @brief Fills holes in a run-length encoded binary image +* +* The function fills holes in the input binary image. A hole is defined as a background region surrounded +* by connected foreground pixels. +* +* @param rlSrc input run-length encoded binary image +* @param rlDest output run-length encoded image with holes filled +* @param connectivity connectivity for hole filling, either 4 or 8 (default: 8) +* +*/ +CV_EXPORTS void fillHoles(InputArray rlSrc, OutputArray rlDest, int connectivity = 8); + //! @} } diff --git a/modules/ximgproc/src/run_length_morphology.cpp b/modules/ximgproc/src/run_length_morphology.cpp index 2819f51ec4a..0db3c504318 100644 --- a/modules/ximgproc/src/run_length_morphology.cpp +++ b/modules/ximgproc/src/run_length_morphology.cpp @@ -36,6 +36,7 @@ #include "precomp.hpp" #include #include +#include #include @@ -807,6 +808,150 @@ CV_EXPORTS void morphologyEx(InputArray rlSrc, OutputArray rlDest, int op, Input } } +static void getNeighborLookup(std::vector& lutNeighbor, int connectivity, const rlVec& runs, const std::vector& pIdxChord1, const std::vector& pIdxNextRow, int emptyValue) +{ + CV_Assert(connectivity == 8 || connectivity == 4); + CV_Assert(lutNeighbor.size() != 0 && lutNeighbor.size() == 4u * runs.size()); + CV_Assert(runs.back().r - runs[0].r + 1 == (int)pIdxChord1.size() && pIdxChord1.size() == pIdxNextRow.size()); + const int connectionOffset = (connectivity == 4) ? 1 : 0; + int nRows = (int)pIdxChord1.size(); + for (int i = 0; i < nRows - 1; ++i) + { + int firstRowStart = pIdxChord1[i]; + int secondRowStart = pIdxChord1[i + 1]; + int firstRowEnd = pIdxNextRow[i] - 1; + int secondRowEnd = pIdxNextRow[i + 1] - 1; + if (firstRowStart != emptyValue && secondRowStart != emptyValue) + { + while (firstRowStart <= firstRowEnd && secondRowStart <= secondRowEnd) + { + const rlType& first = runs[firstRowStart]; + const rlType& second = runs[secondRowStart]; + // Compare runs between two consecutive rows sequentially + if ((first.ce + connectionOffset < second.cb)) + { + firstRowStart++; + } + else if ((first.cb > second.ce + connectionOffset)) + { + secondRowStart++; + } + else + { + // Each run stores indices of its 4-connected neighbors in lutNeighbor: + // [0] = Top-leftmost neighbor + // [1] = Top-rightmost neighbor + // [2] = Bottom-leftmost neighbor + // [3] = Bottom-rightmost neighbor + lutNeighbor[firstRowStart * 4 + 3] = secondRowStart; + lutNeighbor[secondRowStart * 4 + 1] = firstRowStart; + if (lutNeighbor[firstRowStart * 4 + 2] == emptyValue) + { + lutNeighbor[firstRowStart * 4 + 2] = secondRowStart; + } + if (lutNeighbor[secondRowStart * 4] == emptyValue) + { + lutNeighbor[secondRowStart * 4] = firstRowStart; + } + if ((first.ce == second.ce)) + { + firstRowStart++; + secondRowStart++; + } + else if ((first.ce > second.ce)) + { + secondRowStart++; + } + else + { + firstRowStart++; + } + } + } + } + } +} + +// @author Yu Changming, yuchangminghit@gmail.com, https://github.com/yuchangminghit +CV_EXPORTS void fillHoles(InputArray rlSrc, OutputArray rlDest, int connectivity) +{ + CV_Assert(connectivity == 8 || connectivity == 4); + rlVec runsSource, runsDestination; + Size sizeSource; + convertInputArrayToRuns(rlSrc, runsSource, sizeSource); + if ((int)runsSource.size() <= 3) + { + convertToOutputArray(runsSource, sizeSource, rlDest); + return; + } + rlVec borderRect, backgroundRuns; + cv::Rect boundingRect = getBoundingRectangle(runsSource); + if (boundingRect.width < 3 || boundingRect.height < 3) + { + convertToOutputArray(runsSource, sizeSource, rlDest); + return; + } + createUprightRectangle(cv::Rect(boundingRect.x - 1, boundingRect.y - 1, boundingRect.width + 2, boundingRect.height + 2), borderRect); + subtract_rle(borderRect, runsSource, backgroundRuns); + int nMinRow = backgroundRuns[0].r; + int nMaxRow = backgroundRuns.back().r; + int nRows = nMaxRow - nMinRow + 1; + const int EMPTY = -1; + std::vector pIdxChord1(nRows, EMPTY); + std::vector pIdxNextRow(nRows, EMPTY); + pIdxChord1[0] = 0; + pIdxNextRow[nRows - 1] = (int)backgroundRuns.size(); + for (int i = 1; i < (int)backgroundRuns.size(); i++) { + if (backgroundRuns[i].r != backgroundRuns[i - 1].r) { + pIdxChord1[backgroundRuns[i].r - nMinRow] = i; + pIdxNextRow[backgroundRuns[i - 1].r - nMinRow] = i; + } + } + std::vector neighborLUT(4u * backgroundRuns.size(), EMPTY); + getNeighborLookup(neighborLUT, connectivity, backgroundRuns, pIdxChord1, pIdxNextRow, EMPTY); + // Stack stores run indices to be processed + std::stack indexStack; + indexStack.push(0); + // Flags for each run: + // 0: Run is adjacent to the outer background (not part of a hole) + // 255: Run is isolated from the outer background (part of a hole region) + std::vector selectedFlags(backgroundRuns.size(), EMPTY); + const int SELECTED = 0; + selectedFlags[0] = SELECTED; + const int neighborOffsets[] = { 0, 2 }; + while (!indexStack.empty()) + { + int currentIdx = indexStack.top(); + indexStack.pop(); + for (int i = 0; i < 2; i++) + { + if (neighborLUT[4 * currentIdx + neighborOffsets[i]] != EMPTY) + { + int startIdx = neighborLUT[4 * currentIdx + neighborOffsets[i]]; + int endIdx = neighborLUT[4 * currentIdx + neighborOffsets[i] + 1]; + for (int nbrIdx = startIdx; nbrIdx <= endIdx; ++nbrIdx) + { + if (selectedFlags[nbrIdx] != SELECTED) + { + selectedFlags[nbrIdx] = SELECTED; + indexStack.push(nbrIdx); + } + } + } + } + } + rlVec holesBackground; + for (int i = 0; i < (int)selectedFlags.size(); i++) + { + if (selectedFlags[i] == SELECTED) + { + holesBackground.push_back(backgroundRuns[i]); + } + } + invertRegion(holesBackground, runsDestination); + convertToOutputArray(runsDestination, sizeSource, rlDest); +} + } } //end of cv::ximgproc } //end of cv diff --git a/modules/ximgproc/test/test_run_length_morphology.cpp b/modules/ximgproc/test/test_run_length_morphology.cpp index f4e39f48ddd..1f134229d37 100644 --- a/modules/ximgproc/test/test_run_length_morphology.cpp +++ b/modules/ximgproc/test/test_run_length_morphology.cpp @@ -245,5 +245,31 @@ TEST_P(RL_Paint, same_result) INSTANTIATE_TEST_CASE_P(TypicalSET, RL_Paint, Values(CV_8U, CV_16U, CV_16S, CV_32F, CV_64F)); +static void getRuns(std::vector& runs, const cv::Mat& rleImage) +{ + runs.clear(); + cv::Point3i* ptr = (cv::Point3i*)rleImage.ptr(0); + for (int i = 1; i < rleImage.rows; i++) + { + runs.push_back(ptr[i]); + } +} + +TEST(RL_FILL_HOLES, accuracyFillHoles) +{ + std::vector runsSrc = { Point3i(1,4,0),Point3i(0,0,1),Point3i(2,2,1), Point3i(4,4,1), Point3i(1,4,2), Point3i(2,2,3), Point3i(4,4,3), Point3i(3,3,4) }; + std::vector runsDest8 = { Point3i(1,4,0),Point3i(0,4,1),Point3i(1,4,2), Point3i(2,4,3), Point3i(3,3,4) }; + std::vector runsDest4 = { Point3i(1,4,0),Point3i(0,0,1),Point3i(2,4,1), Point3i(1,4,2), Point3i(2,2,3), Point3i(4,4,3), Point3i(3,3,4) }; + Mat rleSrc, rleDest8, rleDest4; + rl::createRLEImage(runsSrc, rleSrc); + rl::fillHoles(rleSrc, rleDest8, 8); + rl::fillHoles(rleSrc, rleDest4, 4); + std::vector runsDest8Get, runsDest4Get; + getRuns(runsDest8Get, rleDest8); + getRuns(runsDest4Get, rleDest4); + EXPECT_EQ(runsDest8Get, runsDest8); + EXPECT_EQ(runsDest4Get, runsDest4); +} + } } \ No newline at end of file From b64482542f468731c52001ef60442f390385d2b4 Mon Sep 17 00:00:00 2001 From: Yu Changming Date: Tue, 15 Jul 2025 21:51:56 +0800 Subject: [PATCH 2/2] Fix C4245 warning --- modules/ximgproc/src/run_length_morphology.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ximgproc/src/run_length_morphology.cpp b/modules/ximgproc/src/run_length_morphology.cpp index 0db3c504318..c7dac2dd30b 100644 --- a/modules/ximgproc/src/run_length_morphology.cpp +++ b/modules/ximgproc/src/run_length_morphology.cpp @@ -915,7 +915,7 @@ CV_EXPORTS void fillHoles(InputArray rlSrc, OutputArray rlDest, int connectivity // Flags for each run: // 0: Run is adjacent to the outer background (not part of a hole) // 255: Run is isolated from the outer background (part of a hole region) - std::vector selectedFlags(backgroundRuns.size(), EMPTY); + std::vector selectedFlags(backgroundRuns.size(), (uchar)255); const int SELECTED = 0; selectedFlags[0] = SELECTED; const int neighborOffsets[] = { 0, 2 };