1 | initial version |
Ho do you define the area to be filled from the area that wont ? Using shape, size, position ?
Here my attempts to fills all holes that are smaller than a given size. Add border would be required if black objects touch image border... and many optimization can be done
Get a mask for small details with TOP HAT. size
void FillSmallHolesTopHat()
{
cv::Mat src, dst, gray, bw, mask;
src = imread("../img/holes.png");
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
int size = 9; // greater than largest hole to fill
cv::Point anchor = cv::Point(size, size);
cv::Mat element = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(bw, mask, MORPH_TOPHAT, element, anchor);
imwrite("../img/topHat.jpg", mask);
//clear a bit
cv::Mat kernel = Mat::ones(3, 3, CV_8UC1);
kernel.at<uchar>(1, 1) = 0;
morphologyEx(mask, mask, MORPH_ERODE, kernel, cv::Point(1, 1));
morphologyEx(mask, mask, MORPH_DILATE, kernel, cv::Point(1, 1));
imwrite("../img/removeIsolated.jpg", mask);
//Fill holes and mask
FillHoles(bw, bw);
imwrite("../img/fillholes.jpg", bw);
// Get result with logical OR
bitwise_or(bw, mask, dst);
imwrite("../img/result-tophat.jpg", dst);
}
alternative using contours
void FillSmallHolesContours()
{
cv::Mat src, dst, gray, bw, mask;
src = imread("../img/holes.png");
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
// Closing the inter space using morph
int size = 8; // greater than largest hole to fill
cv::Point anchor = cv::Point(size, size);
cv::Mat kernel = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(bw, mask, MORPH_ERODE, kernel, anchor);
imwrite("../img/erode.jpg", mask);
//Find and and fill inner contours in black
std::vector<std::vector<cv::Point> > contours;
vector<Vec4i> hierarchy;
findContours(mask, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
int idx = 0;
mask = 255;//white background mask
Scalar color = 0;
for (; idx >= 0; idx = hierarchy[idx][0])
{
bool hasSon = (hierarchy[idx][2]> 0);
bool hasFather = (hierarchy[idx][3] > 0);
if ((hasFather == false) && (hasSon == true))continue;
drawContours(mask, contours, idx, color, CV_FILLED, 8, hierarchy);
}
//restore size for filled holes
size += 2;
anchor = cv::Point(size, size);
kernel = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(mask, mask, MORPH_ERODE, kernel, anchor);
imwrite("../img/innerfill.jpg", mask);
// get the result using logical and
bitwise_and(bw, mask, dst);
imwrite("../img/result-contour.jpg", dst);
}
2 | No.2 Revision |
Ho How do you define the area holes to be filled from the area that wont ? Using shape, size, position ?
Here my attempts to fills all fill in black those white holes that are smaller larger than a given size.
Add border would be is required if black objects touch image border... and many optimization can be done
Modified source image (small holes won't be filled)
1st solution
Create a mask for small holes (holes in white) than add them to flooded source:
void FillSmallHolesTopHat(const cv::Mat &src)
{
cv::Mat dst, gray, bw, mask;
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
// Get a white mask for small details with TOP HAT. size void FillSmallHolesTopHat()
{
cv::Mat src, dst, gray, bw, mask;
src = imread("../img/holes.png");
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
HAT
int size = 9; // hole_to_keep_size = 16; // just greater than largest smallest hole to do not fill
int size = hole_to_keep_size / 2;
cv::Point anchor = cv::Point(size, size);
cv::Mat element = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(bw, mask, MORPH_TOPHAT, element, anchor);
imwrite("../img/topHat.jpg", mask);
//clear a bit
// Remove very small objects from the mask
cv::Mat kernel = Mat::ones(3, 3, CV_8UC1);
kernel.at<uchar>(1, 1) = 0;
morphologyEx(mask, mask, MORPH_ERODE, kernel, cv::Point(1, 1));
morphologyEx(mask, mask, MORPH_DILATE, kernel, cv::Point(1, 1));
imwrite("../img/removeIsolated.jpg", mask);
//Fill holes and mask
// FloodFill on source image
FillHoles(bw, bw);
imwrite("../img/fillholes.jpg", bw);
// Get the result with logical OR
bitwise_or(bw, mask, dst);
imwrite("../img/result-tophat.jpg", dst);
}
alternative using contours
2nd solution
Create a mask for large holes (holes in black) and remove them from source.
void FillSmallHolesContours()
FillSmallHolesContours(const cv::Mat &src)
{
cv::Mat src, dst, gray, bw, mask;
src = imread("../img/holes.png");
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
// Closing the inter space Remove small holes (fill them using morph
morph)
int size = 8; // hole_to_keep_size = 16; // just greater than largest smallest hole to do not fill
int size = hole_to_keep_size / 2;
cv::Point anchor = cv::Point(size, size);
cv::Mat kernel = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(bw, mask, MORPH_ERODE, kernel, anchor);
imwrite("../img/erode.jpg", mask);
//Find and and fill inner //Get contours in black
of the remaining (large) holes
std::vector<std::vector<cv::Point> > contours;
vector<Vec4i> hierarchy;
findContours(mask, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
// Fill inner holes with black
int idx = 0;
mask = 255;//white background mask
Scalar color = 0;
for (; idx >= 0; idx = hierarchy[idx][0])
{
bool hasSon = (hierarchy[idx][2]> 0);
bool hasFather = (hierarchy[idx][3] > 0);
if ((hasFather == false) && (hasSon == true))continue;
drawContours(mask, contours, idx, color, CV_FILLED, 8, hierarchy);
}
//restore // restore size for filled holes
size += 2;
anchor = cv::Point(size, size);
kernel = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(mask, mask, MORPH_ERODE, kernel, anchor);
imwrite("../img/innerfill.jpg", mask);
// get the result using logical and
AND
bitwise_and(bw, mask, dst);
imwrite("../img/result-contour.jpg", dst);
}
3 | No.3 Revision |
How do you define holes to be filled ? Using shape, size, position ?
EDIT: after user specification:
If there are multiple holes in a blob, verify if one of them is enclosed by the others, in which case do not fill
"is enclosed by the others" isn't so clear to me, btw my idea is to find a convex hull around holes from same blob and fill those holes that touch the hull. Easy to say, less easy to do...
Source image
convex hull in red (single or double holes in blue)
result
here my implementation, some optimization could be done ... I hope it's useful
void FillHolesHull(const cv::Mat &src, cv::Mat &dstBw)
{
string funName = "FillHolesHull";
cv::Mat gray, bw, contourMask, hullMask;
#ifdef _DEBUG
Mat dbg;
src.copyTo(dbg);
#endif
cv::cvtColor(src, gray, CV_BGR2GRAY);
int thr = 200;
bw = gray < 200; //THRESH_BINARY_INV: holes in black
dstBw = 255 - bw; //THRESH_BINARY
//find contours
contourMask = Mat::zeros(bw.size(), CV_8UC1);
hullMask = Mat::zeros(bw.size(), CV_8UC1);
vector<vector<cv::Point> > contours;
vector<Vec4i> hierarchy;
findContours(bw, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
//for each contour create a vector of children
//and draw the parents contour in its map
map<int, vector<int> > children;
int maskTh = 2; //IMPORTANT !!! use 2px to flow contour line into the hole
for (int i = 0; i < contours.size(); i++)
{
#ifdef _DEBUG
//putText(dbg, to_string(i), contours[i][0], 1, 1, color);
#endif
int child = hierarchy[i][2];
int parent = hierarchy[i][3];
if (parent < 0)
continue;
children[parent].push_back(i);
drawContours(contourMask, contours, i, 255, maskTh);
}
//for each parent contour
map<int, vector<int>>::iterator it;
for (it = children.begin(); it != children.end(); it++)
{
int idx = it->first;
int cnt = it->second.size();
//fill contours if have 1 or 2 children
if ((cnt > 0) && (cnt < 3))
{
drawContours(dstBw, contours, idx, 0, CV_FILLED);
#ifdef _DEBUG
drawContours(dbg, contours, idx, Scalar(255, 0, 0), CV_FILLED);
#endif
continue;
}
// HERE WHEN 3 OR MORE CHILDREN
// utility: list of children contours for this parent
vector<int> *brothers = &(it->second);
// group all brothers as single contour
vector<cv::Point> allHolesContour;
for (int i = 0; i < brothers->size(); i++)
{
idx = brothers->at(i); //contour index for i-th brother
allHolesContour.insert(allHolesContour.end(), contours[idx].begin(), contours[idx].end());
}
//create convex hull around all brothers
vector<Point> hull;
convexHull(allHolesContour, hull, false);
//draw the hull in its mask
cv::polylines(hullMask, hull, true, 255, maskTh);
#ifdef _DEBUG
cv::polylines(dbg, hull, true, Scalar(0, 0, 255), 1);
#endif
}
// locate brothers that touch the convexhull
// logical AND between thee convexhull and holes itself
bitwise_and(hullMask, contourMask, contourMask);
// cut out black pixel in the source
bitwise_and(dstBw, contourMask, contourMask);
// use this mask as seed for flood fill
uchar *pMask;
int nr = contourMask.rows;
int nc = contourMask.cols;
for (int r = 0; r < nr; ++r)
{
pMask = contourMask.ptr<uchar>(r);
for (int c = 0; c< nc; ++c)
{
if (pMask[c]>0)
floodFill(dstBw, Point(c, r), 0);
}
}
imshow(funName + ":SRC", src);
imshow(funName + ":DST-BW", dstBw);
#ifdef _DEBUG
imshow(funName + ":dbg", dbg);
#endif
return;
}
Old answer and solutions below.
Here my attempts to fill in black those white holes that are larger than a given size. Add border is required if black objects touch image border... and many optimization can be done
Modified source image (small holes won't be filled)
1st solution
Create a mask for small holes (holes in white) than add them to flooded source:
void FillSmallHolesTopHat(const cv::Mat &src)
{
cv::Mat dst, gray, bw, mask;
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
// Get a white mask for small details with TOP HAT
int hole_to_keep_size = 16; // just greater than smallest hole to do not fill
int size = hole_to_keep_size / 2;
cv::Point anchor = cv::Point(size, size);
cv::Mat element = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(bw, mask, MORPH_TOPHAT, element, anchor);
imwrite("../img/topHat.jpg", mask);
// Remove very small objects from the mask
cv::Mat kernel = Mat::ones(3, 3, CV_8UC1);
kernel.at<uchar>(1, 1) = 0;
morphologyEx(mask, mask, MORPH_ERODE, kernel, cv::Point(1, 1));
morphologyEx(mask, mask, MORPH_DILATE, kernel, cv::Point(1, 1));
imwrite("../img/removeIsolated.jpg", mask);
// FloodFill on source image
FillHoles(bw, bw);
imwrite("../img/fillholes.jpg", bw);
// Get the result with logical OR
bitwise_or(bw, mask, dst);
imwrite("../img/result-tophat.jpg", dst);
}
2nd solution
Create a mask for large holes (holes in black) and remove them from source.
void FillSmallHolesContours(const cv::Mat &src)
{
cv::Mat dst, gray, bw, mask;
cv::cvtColor(src, gray, CV_BGR2GRAY);
bw = gray > 200;
// Remove small holes (fill them using morph)
int hole_to_keep_size = 16; // just greater than smallest hole to do not fill
int size = hole_to_keep_size / 2;
cv::Point anchor = cv::Point(size, size);
cv::Mat kernel = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(bw, mask, MORPH_ERODE, kernel, anchor);
imwrite("../img/erode.jpg", mask);
//Get contours of the remaining (large) holes
std::vector<std::vector<cv::Point> > contours;
vector<Vec4i> hierarchy;
findContours(mask, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
// Fill inner holes with black
int idx = 0;
mask = 255;//white background mask
Scalar color = 0;
for (; idx >= 0; idx = hierarchy[idx][0])
{
bool hasSon = (hierarchy[idx][2]> 0);
bool hasFather = (hierarchy[idx][3] > 0);
if ((hasFather == false) && (hasSon == true))continue;
drawContours(mask, contours, idx, color, CV_FILLED, 8, hierarchy);
}
// restore size for filled holes
size += 2;
anchor = cv::Point(size, size);
kernel = getStructuringElement(MORPH_RECT, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(mask, mask, MORPH_ERODE, kernel, anchor);
imwrite("../img/innerfill.jpg", mask);
// get the result using logical AND
bitwise_and(bw, mask, dst);
imwrite("../img/result-contour.jpg", dst);
}