OpenCV實(shí)戰(zhàn)記錄之基于分水嶺算法的圖像分割
0. 前言
分水嶺變換是一種流行的圖像處理算法,用于快速將圖像分割成同質(zhì)區(qū)域。分水嶺變換主要基于以下思想:當(dāng)圖像被視為拓?fù)涓〉駮r(shí),均質(zhì)區(qū)域?qū)?yīng)于相對(duì)平坦且由陡峭的邊緣界定的盆地。算法的原始版本傾向于過(guò)度分割圖像,從而產(chǎn)生多個(gè)小區(qū)域,因此 OpenCV 中實(shí)現(xiàn)了該算法的改進(jìn)版本,通過(guò)使用一組預(yù)定義的標(biāo)記來(lái)指導(dǎo)圖像分割區(qū)域的定義。
1. 分水嶺算法
分水嶺分割可以通過(guò)使用 cv::watershed 函數(shù)實(shí)現(xiàn),函數(shù)的輸入是一個(gè) 32 位有符號(hào)整數(shù)標(biāo)記圖像,其中每個(gè)非零像素表示一個(gè)標(biāo)簽。
通過(guò)標(biāo)記圖像中已知屬于給定區(qū)域的一些像素,利用初始標(biāo)記,分水嶺算法可以確定其他像素所屬的區(qū)域。
(1) 首先,將標(biāo)記圖像讀取為灰度圖像,然后將其轉(zhuǎn)換為整數(shù)類型:
class WatershedSegmentater {
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage) {
// 轉(zhuǎn)換數(shù)據(jù)類型
markerImage.convertTo(markers, CV_32S);
}
cv::Mat process(const cv::Mat& image) {
// 應(yīng)用分水嶺算法
cv::watershed(image, markers);
return markers;
}
有多種獲取標(biāo)記的方式,例如,使用預(yù)處理步驟識(shí)別出屬于感興趣對(duì)象的某些像素,然后利用分水嶺算法根據(jù)初始標(biāo)記分割完整的對(duì)象。在本節(jié)中,我們將使用二值圖像來(lái)識(shí)別相應(yīng)原始圖像中的動(dòng)物。因此,從二值圖像中,我們需要識(shí)別屬于前景(動(dòng)物)的像素和屬于背景(主要是雪地)的像素,我們用標(biāo)簽 255 標(biāo)記前景像素,用標(biāo)簽 128 標(biāo)記背景像素,其他像素則標(biāo)記為 0。
(2) 初始二值圖像包含過(guò)多屬于圖像各個(gè)部分的白色像素,為了只保留屬于重要對(duì)象的像素,我們首先需要腐蝕該圖像:
// 消除噪音 cv::Mat fg; cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 4);
結(jié)果如下圖所示:

(3) 圖中仍然存在一些屬于背景(雪地)的像素,我們通過(guò)對(duì)原始二值圖像進(jìn)行膨脹來(lái)選擇幾個(gè)屬于背景的像素:
// 標(biāo)記圖像像素 cv::Mat bg; cv::dilate(binary, bg, cv::Mat(), cv::Point(-1, -1), 4); cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
結(jié)果如下圖所示,黑色像素對(duì)應(yīng)于背景像素:

(4) 將這些圖像組合起來(lái)形成標(biāo)記圖像:
cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0)); markers = fg+bg;
我們使用重載的 + 運(yùn)算符來(lái)組合圖像,得到用作分水嶺算法的輸入:

(5) 在這個(gè)輸入圖像中,白色區(qū)域?qū)儆谇熬皩?duì)象,灰色區(qū)域是背景的一部分,黑色區(qū)域則屬于未知標(biāo)簽,得到分割結(jié)果如下:
// 創(chuàng)建分水嶺分割對(duì)象 WatershedSegmentater segmenter; segmenter.setMarkers(markers); segmenter.process(image);
更新標(biāo)記圖像,以便為黑色區(qū)域中的像素重新分配標(biāo)簽,而屬于邊界的像素的值為 -1。結(jié)果標(biāo)簽圖像如下:

圖像中對(duì)象邊緣的可視化結(jié)果如下圖所示:

2. 分水嶺算法直觀理解
我們使用拓?fù)鋱D進(jìn)行類比,為了創(chuàng)建分水嶺分割,我們從級(jí)別 0 開始注水,隨著水位逐漸增加,就形成了集水盆地。這些盆地的大小也會(huì)逐漸增加,兩個(gè)不同盆地的水最終會(huì)匯合,發(fā)生這種情況時(shí),會(huì)創(chuàng)建一個(gè)分水嶺,以將兩個(gè)盆地分開。一旦水位達(dá)到最高水位,這些水域和分水嶺就形成了分水嶺分割。
在注水過(guò)程中最初會(huì)產(chǎn)生許多小盆地,當(dāng)這些盆地進(jìn)行合并時(shí),會(huì)創(chuàng)建許多分水嶺線,從而導(dǎo)致圖像被過(guò)度分割。為了克服這個(gè)問題,已經(jīng)提出了多種改進(jìn)算法,在 OpenCV 調(diào)用 cv::watershed 函數(shù)時(shí),注水過(guò)程從一組預(yù)定義的標(biāo)記像素開始,根據(jù)分配給初始標(biāo)記的值對(duì)盆地進(jìn)行標(biāo)記,當(dāng)具有相同標(biāo)簽的兩個(gè)盆地合并時(shí),不會(huì)創(chuàng)建分水嶺,從而防止過(guò)度分割,更新輸入標(biāo)記圖像以獲得最終的分水嶺分割。用戶可以輸入帶有任意數(shù)量的標(biāo)簽和未知標(biāo)簽的標(biāo)記圖像,標(biāo)記圖像的像素類型為為 32 位有符號(hào)整數(shù),以便能夠定義超過(guò) 255 個(gè)標(biāo)簽。cv::watershed 函數(shù)還允許返回與分水嶺關(guān)聯(lián)的像素(使用特殊值 -1 進(jìn)行標(biāo)記)。
為了便于顯示結(jié)果,我們引入兩種特殊的方法。第一個(gè)方法 getSegmentation() 通過(guò)閾值返回標(biāo)簽圖像,分水嶺值為 0:
// 返回結(jié)果
cv::Mat getSegmentation() {
cv::Mat tmp;
markers.convertTo(tmp, CV_8U);
return tmp;
}
第二種方法 getWatersheds() 返回的圖像中,分水嶺線使用值 0 進(jìn)行標(biāo)記,圖像的其余部分像素值為 255,可以使用 cv::convertTo 方法實(shí)現(xiàn):
// 返回分水嶺
cv::Mat getWatersheds() {
cv::Mat tmp;
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}
在轉(zhuǎn)換之前應(yīng)用線性變換,可以將像素值 -1 轉(zhuǎn)換為 0 ( − 1 × 255 + 255 = 0 -1\times 255+255=0 −1×255+255=0)。由于將有符號(hào)整數(shù)轉(zhuǎn)換為無(wú)符號(hào)字符時(shí)需應(yīng)用飽和操作,大于 255 的像素值將轉(zhuǎn)換為 255。
我們也可以通過(guò)許多不同的方式獲得標(biāo)記圖像。例如,可以令用戶以交互方式在圖像中標(biāo)記屬于對(duì)象和背景的像素區(qū)域;或者,如果我們需要識(shí)別位于圖像中心的物體,可以輸入一個(gè)中心區(qū)域標(biāo)有特定標(biāo)簽的圖像,且圖像背景標(biāo)記帶有另一個(gè)標(biāo)簽,可以按以下方式創(chuàng)建標(biāo)記圖像:
// 標(biāo)記背景像素
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);
// 標(biāo)記前景像素
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);
如果我們將此標(biāo)記圖像疊加在測(cè)試圖像上,可以得到以下圖像:

生成的分水嶺圖像如下圖所示:

3. 完整代碼
頭文件 (watershedSegmentation.h) 完整代碼如下:
#if !defined WATERSHS
#define WATERSHS
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class WatershedSegmentater {
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage) {
// 轉(zhuǎn)換數(shù)據(jù)類型
markerImage.convertTo(markers, CV_32S);
}
cv::Mat process(const cv::Mat& image) {
// 應(yīng)用分水嶺算法
cv::watershed(image, markers);
return markers;
}
// 返回結(jié)果
cv::Mat getSegmentation() {
cv::Mat tmp;
markers.convertTo(tmp, CV_8U);
return tmp;
}
// 返回分水嶺
cv::Mat getWatersheds() {
cv::Mat tmp;
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}
};
#endif
主文件 (segment.cpp) 完整代碼如下所示:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include "watershedSegmentation.h"
int main() {
// 讀取輸入圖像
cv::Mat image = cv::imread("1.png");
if (!image.data) return 0;
cv::namedWindow("Original Image");
cv::imshow("Original Image",image);
// 讀取二值圖像
cv::Mat binary;
binary = cv::imread("binary.png", 0);
cv::namedWindow("Binary Image");
cv::imshow("Binary Image", binary);
// 消除噪音
cv::Mat fg;
cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 4);
cv::namedWindow("Foreground Image");
cv::imshow("Foreground Image", fg);
// 標(biāo)記圖像像素
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::namedWindow("Background Image");
cv::imshow("Background Image", bg);
cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
markers = fg+bg;
cv::namedWindow("Markers");
cv::imshow("Markers", markers);
// 創(chuàng)建分水嶺分割對(duì)象
WatershedSegmentater segmenter;
segmenter.setMarkers(markers);
segmenter.process(image);
cv::namedWindow("Segmentation");
cv::imshow("Segmentation", segmenter.getSegmentation());
cv::namedWindow("Watersheds");
cv::imshow("Watersheds", segmenter.getWatersheds());
// 打開另一張圖像
image = cv::imread("3.png");
// 標(biāo)記背景像素
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);
// 標(biāo)記前景像素
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);
segmenter.setMarkers(imageMask);
segmenter.process(image);
cv::rectangle(image,
cv::Point(5, 5),
cv::Point(image.cols-5, image.rows-5),
cv::Scalar(255, 255, 255),
3);
cv::rectangle(image,
cv::Point(image.cols/2-10, image.rows/2-10),
cv::Point(image.cols/2+10, image.rows/2+10),
cv::Scalar(1, 1, 1),
10);
cv::namedWindow("Image with marker");
cv::imshow("Image with marker", image);
cv::namedWindow("Watershed");
cv::imshow("Watershed", segmenter.getWatersheds());
cv::waitKey();
return 0;
}
總結(jié)
到此這篇關(guān)于OpenCV實(shí)戰(zhàn)記錄之基于分水嶺算法的圖像分割的文章就介紹到這了,更多相關(guān)OpenCV分水嶺算法的圖像分割內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 使用Python和OpenCV進(jìn)行視覺圖像分割的代碼示例
- Tensorflow2.10實(shí)現(xiàn)圖像分割任務(wù)示例詳解
- Python基于紋理背景和聚類算法實(shí)現(xiàn)圖像分割詳解
- OpenCV利用K-means實(shí)現(xiàn)根據(jù)顏色進(jìn)行圖像分割
- 詳解Python OpenCV圖像分割算法的實(shí)現(xiàn)
- python中的opencv?圖像分割與提取
- Python 深入了解opencv圖像分割算法
- openCV實(shí)現(xiàn)圖像分割
- 計(jì)算機(jī)視覺中圖像分割怎么學(xué)?從入門到實(shí)踐的核心技術(shù)全解析
相關(guān)文章
Python中的標(biāo)簽編碼和獨(dú)熱編碼示例詳解
標(biāo)簽編碼是一種用于將分類列轉(zhuǎn)換為數(shù)值列的技術(shù),以便它們可以通過(guò)僅采用數(shù)值數(shù)據(jù)的機(jī)器學(xué)習(xí)模型進(jìn)行擬合,這篇文章主要介紹了Python中的標(biāo)簽編碼和獨(dú)熱編碼,需要的朋友可以參考下2023-07-07
Python數(shù)據(jù)清洗之缺失值處理,重復(fù)項(xiàng)去重與異常值檢測(cè)詳解
本文主要為大家詳細(xì)介紹了Python數(shù)據(jù)清洗中的核心技能,包括缺失值處理、重復(fù)數(shù)據(jù)去重和異常值檢測(cè),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2026-05-05
在雙python下設(shè)置python3為默認(rèn)的方法
這篇文章主要介紹了如何在雙python下設(shè)置python3為默認(rèn),本文通過(guò)一個(gè)例子分步驟給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-10-10
Python將Word文檔轉(zhuǎn)換為Markdown格式
Markdown作為一種輕量級(jí)標(biāo)記語(yǔ)言,以其簡(jiǎn)潔的語(yǔ)法和廣泛的兼容性,本文將介紹如何使用Python將Word文檔轉(zhuǎn)換為Markdown文件,需要的可以了解下2024-11-11
使用Python高效讀取ZIP壓縮文件中的JSON數(shù)據(jù)
本文將詳細(xì)介紹如何使用Python快速高效地讀取ZIP壓縮文件中的UTF-8編碼JSON文件,并將其轉(zhuǎn)換為Pandas DataFrame和PySpark DataFrame,感興趣的小伙伴可以了解下2025-10-10
Python使用pyecharts繪制世界地圖,省級(jí)地圖,城市地圖實(shí)例詳解
這篇文章主要介紹了如何使用Python中的pyecharts庫(kù)繪制世界地圖、中國(guó)地圖、省級(jí)地圖、市級(jí)地圖,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起動(dòng)手試一試2022-02-02
python+elasticsearch實(shí)現(xiàn)標(biāo)簽匹配計(jì)數(shù)操作
這篇文章主要介紹了python+elasticsearch實(shí)現(xiàn)標(biāo)簽匹配計(jì)數(shù)操作,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-04-04

