Home > database >  Hole-Filling Filter in OpenCV C
Hole-Filling Filter in OpenCV C

Time:06-11

I have a basic implementation of hole-filling filter as shown below:

#include <iostream>
#include <opencv2/opencv.hpp>

int main(int argc, char** argv)
{
    // please note that the depthImg is (720 x 576) 8UC1
    // let's make a smaller one for testing
    uchar flatten[6 * 8] = { 140, 185,  48, 235, 201, 192, 131,  57,
                              55,  87,  82,   0,   6, 201,   0,  38,
                               6, 239,  82, 142,  46,  33, 172,  72,
                             133,   0, 232, 226,  66,  59,  10, 204,
                             214, 123, 202, 100,   0,  32,   6, 147,
                             105, 191,  50,  21,  87, 117, 118, 244};

    cv::Mat depthImg = cv::Mat(6, 8, CV_8UC1, flatten);

    // please ignore the border pixels in this case
    for (int i = 1; i < depthImg.cols - 1; i  ) {
        for (int j = 1; j < depthImg.rows - 1; j  ) {
            unsigned short sumNonZeroAdjs = 0;
            uchar countNonZeroAdjs = 0;
            if (depthImg.at<uchar>(j, i) == 0) {
                uchar iMinus1 = depthImg.at<uchar>(j, i - 1);
                uchar  iPlus1 = depthImg.at<uchar>(j, i   1);
                uchar jMinus1 = depthImg.at<uchar>(j - 1, i);
                uchar  jPlus1 = depthImg.at<uchar>(j   1, i);
                if (iMinus1 != 0) {
                    sumNonZeroAdjs  = iMinus1;
                    countNonZeroAdjs  ;
                }
                if (iPlus1 != 0) {
                    sumNonZeroAdjs  = iPlus1;
                    countNonZeroAdjs  ;
                }
                if (jMinus1 != 0) {
                    sumNonZeroAdjs  = jMinus1;
                    countNonZeroAdjs  ;
                }
                if (jPlus1 != 0) {
                    sumNonZeroAdjs  = jPlus1;
                    countNonZeroAdjs  ;
                }
                depthImg.at<uchar>(j, i) = sumNonZeroAdjs / countNonZeroAdjs;
            }
        }
    }

    std::cout << depthImg << std::endl;
    return 0;
}
// prints the following:
[140, 185, 48, 235, 201, 192, 131, 57;
  55, 87, 82, 116, 6, 201, 135, 38;
  6, 239, 82, 142, 46, 33, 172, 72;
  133, 181, 232, 226, 66, 59, 10, 204;
  214, 123, 202, 100, 71, 32, 6, 147;
  105, 191, 50, 21, 87, 117, 118, 244]

The above filter computes an average of adjacent pixels to fill the 0 pixels. The output from this implementation is satisfactory. However, as we can see, the above prototype is not elegant and painfully slow.

I am looking for a similar logic (using adjacent pixels to fill 0 pixels) but faster (execution time) hole-filling filter inbuilt in OpenCV

PS: I am using OpenCV v4.2.0 on Ubuntu 20.04 LTS.

Update 1

Based on the suggestions, I designed pointer style access. Complete code is shown below:

#include <iostream>
#include <opencv2/opencv.hpp>

void inPlaceHoleFillingExceptBorderPtrStyle(cv::Mat& img) {
  typedef uchar T;
  T* ptr = img.data;
  size_t elemStep = img.step / sizeof(T);

  for (int i = 1; i < img.rows - 1; i  ) {
    for (int j = 1; j < img.cols - 1; j  ) {
      T& curr = ptr[i * elemStep   j];
      if (curr != 0) {
        continue;
      }

      ushort sumNonZeroAdjs = 0;
      uchar countNonZeroAdjs = 0;
      T iM1 = ptr[(i - 1) * elemStep   j];
      T iP1 = ptr[(i   1) * elemStep   j];
      T jM1 = ptr[i * elemStep   (j - 1)];
      T jP1 = ptr[i * elemStep   (j   1)];

      if (iM1 != 0) {
        sumNonZeroAdjs  = iM1;
        countNonZeroAdjs  ;
      }
      if (iP1 != 0) {
        sumNonZeroAdjs  = iP1;
        countNonZeroAdjs  ;
      }
      if (jM1 != 0) {
        sumNonZeroAdjs  = jM1;
        countNonZeroAdjs  ;
      }
      if (jP1 != 0) {
        sumNonZeroAdjs  = jP1;
        countNonZeroAdjs  ;
      }
      if (countNonZeroAdjs > 0) {
        curr = sumNonZeroAdjs / countNonZeroAdjs;
      }
    }
  }
}

void inPlaceHoleFillingExceptBorder(cv::Mat& img) {
  typedef uchar T;

  for (int i = 1; i < img.cols - 1; i  ) {
    for (int j = 1; j < img.rows - 1; j  ) {
      ushort sumNonZeroAdjs = 0;
      uchar countNonZeroAdjs = 0;
      if (img.at<T>(j, i) != 0) {
        continue;
      }

      T iM1 = img.at<T>(j, i - 1);
      T iP1 = img.at<T>(j, i   1);
      T jM1 = img.at<T>(j - 1, i);
      T jP1 = img.at<T>(j   1, i);

      if (iM1 != 0) {
        sumNonZeroAdjs  = iM1;
        countNonZeroAdjs  ;
      }
      if (iP1 != 0) {
        sumNonZeroAdjs  = iP1;
        countNonZeroAdjs  ;
      }
      if (jM1 != 0) {
        sumNonZeroAdjs  = jM1;
        countNonZeroAdjs  ;
      }
      if (jP1 != 0) {
        sumNonZeroAdjs  = jP1;
        countNonZeroAdjs  ;
      }
      if (countNonZeroAdjs > 0) {
        img.at<T>(j, i) = sumNonZeroAdjs / countNonZeroAdjs;
      }
    }
  }
}

int main(int argc, char** argv) {
  // please note that the img is (720 x 576) 8UC1
  // let's make a smaller one for testing
  // clang-format off
  uchar flatten[6 * 8] = { 140, 185,  48, 235, 201, 192, 131,  57,
                            55,  87,  82,   0,   6, 201,   0,  38,
                             6, 239,  82, 142,  46,  33, 172,  72,
                           133,   0, 232, 226,  66,  59,  10, 204,
                           214, 123, 202, 100,   0,  32,   6, 147,
                           105, 191,  50,  21,  87, 117, 118, 244};
  // clang-format on

  cv::Mat img = cv::Mat(6, 8, CV_8UC1, flatten);
  cv::Mat img1 = img.clone();
  cv::Mat img2 = img.clone();

  inPlaceHoleFillingExceptBorderPtrStyle(img1);
  inPlaceHoleFillingExceptBorder(img2);

  return 0;
}

/*** expected output
[140, 185,  48, 235, 201, 192, 131, 57;
  55,  87,  82, 116,  6,  201, 135, 38;
   6, 239,  82, 142, 46,   33, 172, 72;
 133, 181, 232, 226, 66,   59,  10, 204;
 214, 123, 202, 100, 71,   32,   6, 147;
 105, 191,  50,  21, 87,  117, 118, 244]
***/

Update 2

Based on the suggestion, the point style code is further improved as shown below:


void inPlaceHoleFillingExceptBorderImpv(cv::Mat& img) {
  typedef uchar T;
  size_t elemStep = img.step1();
  const size_t margin = 1;

  for (size_t i = margin; i < img.rows - margin;   i) {
    T* ptr = img.data   i * elemStep;
    for (size_t j = margin; j < img.cols - margin;   j,   ptr) {
      T& curr = ptr[margin];
      if (curr != 0) {
        continue;
      }

      T& north = ptr[margin - elemStep];
      T& south = ptr[margin   elemStep];
      T&  east = ptr[margin   1];
      T&  west = ptr[margin - 1];

      ushort  sumNonZeroAdjs = 0;
      uchar countNonZeroAdjs = 0;
      if (north != 0) {
        sumNonZeroAdjs  = north;
        countNonZeroAdjs  ;
      }
      if (south != 0) {
        sumNonZeroAdjs  = south;
        countNonZeroAdjs  ;
      }
      if (east != 0) {
        sumNonZeroAdjs  = east;
        countNonZeroAdjs  ;
      }
      if (west != 0) {
        sumNonZeroAdjs  = west;
        countNonZeroAdjs  ;
      }
      if (countNonZeroAdjs > 0) {
        curr = sumNonZeroAdjs / countNonZeroAdjs;
      }
    }
  }
}

CodePudding user response:

There are three parts: 1) find the zeros, 2) find the mean, and 3) fill the found zeros with mean. So:

/****
 * in-place fill zeros with the mean of the surrounding neighborhoods
 ***/
void fillHoles(Mat gray){    
    // find the zeros
    Mat mask = (gray == 0);
    
    // find the mean with filter2d
    Mat kernel = (Mat_<double>(3,3) << 
    1/8, 1/8, 1/8
    1/8, 0  , 1/8
    1/8, 1/8, 1/8
    );
    Mat avg;
    cv::filter2d(gray, avg, CV_8U, kernel)
    
    // then fill the zeros, only where indicated by `mask`
    cv::bitwise_or(gray, avg, gray, mask);

}

Note I just realize that this is plainly taking the average, not the non-zero average. For that operation, you might want to do two filters, one for the sum, one for the non-zero counts, then divide the two:

// find the neighborhood sum with filter2d
Mat kernel = (Mat_<double>(3,3) << 
1, 1, 1
1, 0, 1
1, 1, 1
);
Mat sums;
cv::filter2d(gray, sums, CV_64F, kernel);

// find the neighborhood count with filter2d
Mat counts;
cv::filter2d(gray!=0, counts, CV_64F, kernel);    
counts /= 255; // because gray!=0 returns 255 where true

// force counts to 1 if 0, so we can divide later
cv::max(counts, 1, counts);

Mat out;
cv::divide(sums, counts, out);

out.convertTo(gray, CV_8U);

CodePudding user response:

@QuangHoang advised a fantastic kernel-based approach. Unfortunately, the result is not matching with the expected output.

Therefore, based on various comments from @CrisLuengo, I managed to design time efficient version of the filter, as shown below:

void inPlaceHoleFillingExceptBorderFast(cv::Mat& img) {
  typedef uchar T;
  const size_t elemStep = img.step1();
  const size_t margin = 1;

  for (size_t i = margin; i < img.rows - margin;   i) {
    T* ptr = img.data   i * elemStep;
    for (size_t j = margin; j < img.cols - margin;   j,   ptr) {
      T& curr = ptr[margin];
      if (curr != 0) {
        continue;
      }

      T& east  = ptr[margin   1];
      T& west  = ptr[margin - 1];
      T& north = ptr[margin - elemStep];
      T& south = ptr[margin   elemStep];

      // convert to 0/1 and sum them up
      uchar count = static_cast<bool>(north)   static_cast<bool>(south)  
                    static_cast<bool>(east)    static_cast<bool>(west);

      // we do not want to divide by 0
      if (count > 0) {
        curr = (north   south   east   west) / count;
      }
    }
  }
}
  • Related