用分水岭算法实现图像分割 - sumpig/OpenCV GitHub Wiki

如果把图像看作一个拓扑地貌,那么同类区域就相当于陡峭边缘内相对平坦的盆地。分水岭算法通过逐步增高水位,把地貌分割成多个部分。

因为算法很简单,它的原始版本会过度分割图像,产生很多小的区域。因此OpenCV 提出了该算法的改进版本,使用一系列预定义标记来引导图像分割的定义方式。

使用分水岭分割法需要调用 cv::watershed 函数。该函数的输入对象是一个标记图像,图像的像素值为32 位有符号整数,每个非零像素代表一个标签。

不同应用程序获得标记的方式各不相同。主要思想是标记处图像中的部分前景和背景。

// 消除噪声和细小物体
cv::Mat fg;
cv::erode(binary, fg, cv::Mat(), cv::Point(-1,-1), 4);

// 标识不含物体的图像像素
cv::Mat bg;
cv::dilate(binary, bg, cv::Mat(), cv::Point(-1,-1), 4);
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);

// 创建标记图像
cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
markers = fg + bg;

// 创建分水岭分割类的对象
WatershedSegmenter segmenter;

// 设置标记图像,然后执行分割过程
segmenter.setMarkers(markers);
segmenter.process(image);
class WatershedSegmenter {
    private:
        cv::Mat markers;

    public:
        void setMarkers(const cv::Mat& markerImage) {
            // 转换成整数型图像
            markerImage.convertTo(markers, CV_32S);
        }
            
        cv::Mat process(const cv::Mat &image) {
            // 应用分水岭
            cv::watershed(image, markers);
            return markers;
        } 

标签图像:

// 以图像的形式返回结果
cv::Mat getSegmentation() {
    cv::Mat tmp;
    // 所有标签值大于255 的区段都赋值为255
    markers.convertTo(tmp,CV_8U);
    
    return tmp;
}

边缘图像:

// 以图像的形式返回分水岭
cv::Mat getWatersheds() {
    cv::Mat tmp;
    // 在变换前,把每个像素p 转换为255p+255
    markers.convertTo(tmp,CV_8U,255,255);
    
    return tmp;
}

扩展

用户可以交互式地在场景中的物体和背景上绘制区域,以标注物体。

当需要标识的物体位于图像中间时,可以简单地在输入图像的中心位置标记特定标签,在图像的边缘位置(假设背景在边缘位置)标记上另一个标签。在创建标记图像时,可以在标记图像上绘制加粗的矩形:

// 标识背景像素
cv::Mat imageMask(image.size(), CV_8U, cv::Scalar(0));
cv::rectangle(imageMask,cv::Point(5,5),
              cv::Point(image.cols-5, image.rows-5),
              cv::Scalar(255), 3);

// 标识前景像素
// (在图像的中心)
cv::rectangle(imageMask,
              cv::Point(image.cols/2-10, image.rows/2-10),
              cv::Point(image.cols/2+10, image.rows/2+10),
              cv::Scalar(1), 10);