使用OpenCV检测图像中的物体区域

22
我们目前正在尝试使用OpenCV C++版本中提供的方法来检测医疗器械图像中的物体区域。下面是一个示例图像: 1 我们正在按照以下步骤进行:
- 将图像转换为灰度图像 - 应用中值滤波器 - 使用Sobel滤波器找到边缘 - 使用阈值为25将结果转换为二进制图像 - 对图像进行骨架化,以确保我们有整洁的边缘 - 查找X个最大连接组件
这种方法对于图像1完美地奏效了。这是结果: 1-result 黄色边框是检测到的连接组件,矩形只是为了突出显示连接组件的存在。为了得到可理解的结果,我们只需删除完全位于任何其他组件内部的连接组件,因此最终结果如下所示: 1-endResult 到目前为止,一切都很好,但另一个图片样本却让我们的工作变得复杂。 2 在物体下放置小型浅绿色毛巾得到此图像: 2-result 在过滤区域时,我们之前采用的方法不再奏效,所以我们得到了这个结果: 2-endresult 显然,这不是我们需要的结果。我们期望得到像这样的结果: 2-ExpectedResult

我正在考虑对最接近的连接组件进行聚类(不知道怎么做!)以便我们可以尽量减少毛巾存在的影响,但还不确定这是否可行或是否有人尝试过类似的方法?此外,是否有更好的办法来解决这类问题?

先在此谢过。

3个回答

25

这是我尝试过的方法。

在图片中,背景大部分都是绿色的,并且背景区域比前景区域要大得多。因此,如果你对图像进行颜色直方图处理,绿色的条柱将有更高的数值。将这个直方图阈值化以便数值较小的条柱被设为零。这样做我们很可能会保留绿色(数值较高)条柱并且丢弃其他颜色。然后对这个直方图进行反向投影处理。反向投影处理将会突出显示图像中这些绿色区域。

反向投影: backprojection

  • 然后对反向投影进行阈值化。这样我们就得到了背景。

背景(经过一些形态学滤波之后): background

  • 反转背景以得到前景。

前景(经过一些形态学滤波之后): foreground

  • 接着找到前景的轮廓。

我认为这样得到的分割结果还比较合理,使用这个作为掩膜,你或许可以使用像 GrabCut 这样的分割算法来进一步完善边界(我还没有尝试过)。

EDIT: 我尝试了一下 GrabCut 方法,它确实可以完善边界。我已经添加了 GrabCut 分割的代码。

轮廓: contours

使用前景作为掩膜的 GrabCut 分割: gc

在直方图处理部分,我使用了 OpenCV 的 C API。

// load the color image
IplImage* im = cvLoadImage("bFly6.jpg");

// get the color histogram
IplImage* im32f = cvCreateImage(cvGetSize(im), IPL_DEPTH_32F, 3);
cvConvertScale(im, im32f);

int channels[] = {0, 1, 2};
int histSize[] = {32, 32, 32};
float rgbRange[] = {0, 256};
float* ranges[] = {rgbRange, rgbRange, rgbRange};

CvHistogram* hist = cvCreateHist(3, histSize, CV_HIST_ARRAY, ranges);
IplImage* b = cvCreateImage(cvGetSize(im32f), IPL_DEPTH_32F, 1);
IplImage* g = cvCreateImage(cvGetSize(im32f), IPL_DEPTH_32F, 1);
IplImage* r = cvCreateImage(cvGetSize(im32f), IPL_DEPTH_32F, 1);
IplImage* backproject32f = cvCreateImage(cvGetSize(im), IPL_DEPTH_32F, 1);
IplImage* backproject8u = cvCreateImage(cvGetSize(im), IPL_DEPTH_8U, 1);
IplImage* bw = cvCreateImage(cvGetSize(im), IPL_DEPTH_8U, 1);
IplConvKernel* kernel = cvCreateStructuringElementEx(3, 3, 1, 1, MORPH_ELLIPSE);

cvSplit(im32f, b, g, r, NULL);
IplImage* planes[] = {b, g, r};
cvCalcHist(planes, hist);

// find min and max values of histogram bins
float minval, maxval;
cvGetMinMaxHistValue(hist, &minval, &maxval);

// threshold the histogram. this sets the bin values that are below the threshold to zero
cvThreshHist(hist, maxval/32);

// backproject the thresholded histogram. backprojection should contain higher values for the
// background and lower values for the foreground
cvCalcBackProject(planes, backproject32f, hist);

// convert to 8u type
double min, max;
cvMinMaxLoc(backproject32f, &min, &max);
cvConvertScale(backproject32f, backproject8u, 255.0 / max);

// threshold backprojected image. this gives us the background
cvThreshold(backproject8u, bw, 10, 255, CV_THRESH_BINARY);

// some morphology on background
cvDilate(bw, bw, kernel, 1);
cvMorphologyEx(bw, bw, NULL, kernel, MORPH_CLOSE, 2);

// get the foreground
cvSubRS(bw, cvScalar(255, 255, 255), bw);
cvMorphologyEx(bw, bw, NULL, kernel, MORPH_OPEN, 2);
cvErode(bw, bw, kernel, 1);

// find contours of the foreground
//CvMemStorage* storage = cvCreateMemStorage(0);
//CvSeq* contours = 0;
//cvFindContours(bw, storage, &contours);
//cvDrawContours(im, contours, CV_RGB(255, 0, 0), CV_RGB(0, 0, 255), 1, 2);

// grabcut
Mat color(im);
Mat fg(bw);
Mat mask(bw->height, bw->width, CV_8U);

mask.setTo(GC_PR_BGD);
mask.setTo(GC_PR_FGD, fg);

Mat bgdModel, fgdModel;
grabCut(color, mask, Rect(), bgdModel, fgdModel, GC_INIT_WITH_MASK);

Mat gcfg = mask == GC_PR_FGD;

vector<vector<cv::Point>> contours;
vector<Vec4i> hierarchy;
findContours(gcfg, contours, hierarchy, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
for(int idx = 0; idx < contours.size(); idx++)
{
    drawContours(color, contours, idx, Scalar(0, 0, 255), 2);
}

// cleanup ...

更新: 我们可以使用下面展示的 C++ 接口来完成上述操作。

const int channels[] = {0, 1, 2};
const int histSize[] = {32, 32, 32};
const float rgbRange[] = {0, 256};
const float* ranges[] = {rgbRange, rgbRange, rgbRange};

Mat hist;
Mat im32fc3, backpr32f, backpr8u, backprBw, kernel;

Mat im = imread("bFly6.jpg");

im.convertTo(im32fc3, CV_32FC3);
calcHist(&im32fc3, 1, channels, Mat(), hist, 3, histSize, ranges, true, false);
calcBackProject(&im32fc3, 1, channels, hist, backpr32f, ranges);

double minval, maxval;
minMaxIdx(backpr32f, &minval, &maxval);
threshold(backpr32f, backpr32f, maxval/32, 255, THRESH_TOZERO);
backpr32f.convertTo(backpr8u, CV_8U, 255.0/maxval);
threshold(backpr8u, backprBw, 10, 255, THRESH_BINARY);

kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));

dilate(backprBw, backprBw, kernel);
morphologyEx(backprBw, backprBw, MORPH_CLOSE, kernel, Point(-1, -1), 2);

backprBw = 255 - backprBw;

morphologyEx(backprBw, backprBw, MORPH_OPEN, kernel, Point(-1, -1), 2);
erode(backprBw, backprBw, kernel);

Mat mask(backpr8u.rows, backpr8u.cols, CV_8U);

mask.setTo(GC_PR_BGD);
mask.setTo(GC_PR_FGD, backprBw);

Mat bgdModel, fgdModel;
grabCut(im, mask, Rect(), bgdModel, fgdModel, GC_INIT_WITH_MASK);

Mat fg = mask == GC_PR_FGD;

我想感谢您的贡献,这正是我想要的,您值得获得奖励 :) 再次感谢。 - Maystro
@Maystro,请注意更新。我一直在考虑添加这个更新,但错过了机会。现在是最好的时候 :) - dhanushka
1
谢谢,能否也加上缺失的部分? - Maystro

5

我会考虑几个选项。我的假设是相机不会移动。我没有使用过这些图像或编写任何代码,所以这主要是基于经验。

  • 与其只寻找边缘,不如使用分割算法来分离背景。高斯混合可以帮助实现这一点。给定相同区域的一组图像(即视频),您可以消除持续出现的区域。然后,新项目(例如器具)将跳出来。然后可以在斑点上使用连通组件。

    • 我建议您查看分割算法,看看是否可以优化条件使其适用于您的情况。一个重要的项目是确保您的摄像机稳定或在预处理中自行稳定图像。
  • 我会考虑使用兴趣点来识别图像中的区域,并确定其中有很多新物质。鉴于背景相对平坦,小物体,例如针,将创建大量的兴趣点。毛巾应该更稀疏。也许将检测到的兴趣点叠加在连接的组件足迹上将为您提供“密度”指标,然后您可以阈值化。如果连接组件的兴趣点面积比例很大,则它是一个有趣的对象。

    • 在此基础上,您甚至可以使用凸壳来清理连接的组件足迹,以修剪已检测到的对象。这可能有助于医疗器械在毛巾上投射阴影并拉伸组件区域等情况。这只是一个猜测,但兴趣点绝对可以为您提供比边缘更多的信息。
  • 最后,考虑到您拥有稳定的背景和清晰可见的物体,我建议您查看“词袋特征”(Bag-of-Features),以查看是否可以仅检测图像中的每个单独物体。这可能会很有用,因为这些图像中的物体似乎具有一致的模式。您可以构建一个大型的图像数据库,例如针,纱布,剪刀等。然后,OpenCV中的BoF将为您找到那些候选项。您还可以将其与其他操作混合在一起以比较结果。

  • -

谢谢您的回复。实际上,我正在拍摄手术期间仪器的桌子。一开始所有的仪器都放在桌子上,外科医生会逐渐使用它们。因此,我需要知道视频的每个瞬间桌子上有哪些仪器。然后,这与仅检查新来者无关。我会考虑您建议的内容并尝试一些东西,然后再联系您。 - Maystro
鉴于物品在集合的开始处呈现,Bag-of-Features可能比背景分割更好。一种非优雅但经过验证的技术是将物品放在图案上。在航空领域,工具箱需要有每个工具的轮廓线。这样可以快速检查责任。如果您为特定操作准备了一组模式,则可以极大地简化问题。这不是优雅的,但是另一种消除错误的技术。您可以在角落上放置ID标签,以允许您预加载预期的项目。 - msmith81886
谢谢您的贡献,但我只能给您的回答点个赞,因为被采纳的答案已经包含了我需要的代码和所有内容。 - Maystro

0
我还想给你的初始版本提出一个建议。您还可以跳过轮廓,其区域的宽度和高度大于图像宽度和高度的一半。
//take the rect of the contours

Rect rect = Imgproc.boundingRect(contours.get(i));

if (rect.width < inputImageWidth / 2 && rect.height < inputImageHeight / 2)

//then continue to draw or use for next purposes.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接