分水岭算法
视频讲解如下:
在本章节中给大家演示分水岭算法(即:Watershed),当前代码同样的是在毛星云的代码基础上进行扩展优化的。
C#、C++、Python三种版本的效果其实是一样的,但我这里还是单独做了演示。
当前系列所有demo下载地址:
https://github.com/GaoRenBao/OpenCv4-Demo
不同编程语言对应的OpenCv版本以及开发环境信息如下:
语言 | OpenCv版本 | IDE |
C# | OpenCvSharp4.4.8.0.20230708 | Visual Studio 2022 |
C++ | OpenCv-4.5.5-vc14_vc15 | Visual Studio 2022 |
Python | OpenCv-Python (4.6.0.66) | PyCharm Community Edition 2022.1.3 |
首先呢,我们需要准备一张测试图片。(不想下载工程的童鞋,可直接复制下面的图片和代码)

最终演示效果如下,现在原图上画几条线,然后按1或者空格,执行分水岭算法

C#版本代码如下:
C#版本需要安装“OpenCvSharp4”、“OpenCvSharp4.runtime.win”两个库才行。不然会报错。
如果需要使用“ BitmapConverter.ToBitmap”操作,则需要追加安装“OpenCvSharp4.Extensions”库。
OpenCv3版本的SetMouseCallback操作请参考:/Course?id=4743192000049
using OpenCvSharp;
using System;
using System.Collections.Generic;
namespace demo
{
internal class Program
{
public static string WINDOW_NAME1 = "【程序窗口】"; //为窗口标题定义的宏
public static string WINDOW_NAME2 = "【分水岭算法效果图】"; //为窗口标题定义的宏
public static Mat srcImage = new Mat();
public static Mat grayImage = new Mat();
public static Mat g_maskImage = new Mat();
public static Mat g_srcImage = new Mat();
public static Point prevPt = new Point(-1, -1);
public static void on_Mouse(MouseEventTypes @event, int x, int y, MouseEventFlags flags, IntPtr userData)
{
//处理鼠标不在窗口中的情况
if (x < 0 || x >= g_srcImage.Cols || y < 0 || y >= g_srcImage.Rows)
return;
//处理鼠标左键相关消息
if (@event == MouseEventTypes.LButtonUp || ((int)flags & (int)MouseEventTypes.LButtonDown) == 0)
prevPt = new Point(-1, -1);
else if (@event == MouseEventTypes.LButtonDown)
prevPt = new Point(x, y);
//鼠标左键按下并移动,绘制出白色线条
else if (@event == MouseEventTypes.MouseMove && ((int)flags & (int)MouseEventTypes.LButtonDown) > 0)
{
Point pt = new Point(x, y);
if (prevPt.X < 0)
prevPt = pt;
Cv2.Line(g_maskImage, prevPt, pt, new Scalar(255, 255, 255), 5, LineTypes.Link8, 0);
Cv2.Line(g_srcImage, prevPt, pt, new Scalar(255, 255, 255), 5, LineTypes.Link8, 0);
prevPt = pt;
Cv2.ImShow(WINDOW_NAME1, g_srcImage);
}
}
static void Main(string[] args)
{
// 载入原图
g_srcImage = Cv2.ImRead("../../../images/mountain.jpg");
if (g_srcImage.Empty())
{
Console.WriteLine("Could not open or find the image!");
return;
}
Cv2.ImShow(WINDOW_NAME1, g_srcImage);
g_srcImage.CopyTo(srcImage);
Cv2.CvtColor(g_srcImage, g_maskImage, ColorConversionCodes.BGR2GRAY); // 灰度
Cv2.CvtColor(g_maskImage, grayImage, ColorConversionCodes.GRAY2BGR); // 修改颜色空间
g_maskImage.SetIdentity(new Scalar(0));
//【2】设置鼠标操作回调函数
Cv2.NamedWindow(WINDOW_NAME1);
Cv2.SetMouseCallback(WINDOW_NAME1, new MouseCallback(on_Mouse));
RNG rng = new RNG(12345);
while (true)
{
//获取键值
int c = Cv2.WaitKey(0);
// 若按键键值为ESC时,退出
if ((char)c == 27)
break;
//按键键值为2时,恢复源图
if ((char)c == '2')
{
g_maskImage = new Mat(g_maskImage.Size(), g_maskImage.Type());
srcImage.CopyTo(g_srcImage);
Cv2.ImShow(WINDOW_NAME1, g_srcImage);
}
// 若检测到按键值为1或者空格,则进行处理
if ((char)c == '1' || (char)c == ' ')
{
// 定义一些参数
int i, j, compCount = 0;
Point[][] contours;
HierarchyIndex[] hierarchy;
//寻找轮廓
Cv2.FindContours(g_maskImage, out contours, out hierarchy, RetrievalModes.CComp, ContourApproximationModes.ApproxSimple);
//轮廓为空时的处理
if (contours.Length == 0)
continue;
//拷贝掩膜
Mat maskImage = new Mat(g_maskImage.Size(), MatType.CV_32S);
maskImage.SetIdentity(new Scalar(0));
// 循环绘制出轮廓
for (int index = 0; index >= 0; index = hierarchy[index].ToVec4i().Item0, compCount++)
{
//绘制轮廓
Cv2.DrawContours(maskImage, contours, index, new Scalar(compCount + 1), -1, LineTypes.Link8, hierarchy);
}
//compCount为零时的处理
if (compCount == 0)
continue;
//生成随机颜色
List<Vec3b> colorTab = new List<Vec3b>();
for (i = 0; i < compCount; i++)
{
byte b = (byte)rng.Uniform(100, 255);
byte g = (byte)rng.Uniform(100, 255);
byte r = (byte)rng.Uniform(100, 255);
colorTab.Add(new Vec3b(b, g, r));
}
Cv2.Watershed(srcImage, maskImage);
//双层循环,将分水岭图像遍历存入watershedImage中
Mat watershedImage = new Mat(maskImage.Size(), MatType.CV_8UC3);
for (i = 0; i < maskImage.Rows; i++)
{
for (j = 0; j < maskImage.Cols; j++)
{
int index = maskImage.At<int>(i, j);
if (index == -1)
watershedImage.Set<Vec3b>(i, j, new Vec3b(255, 255, 255));
else if (index <= 0 || index > compCount)
watershedImage.Set<Vec3b>(i, j, new Vec3b(0, 0, 0));
else
watershedImage.Set<Vec3b>(i, j, colorTab[index - 1]);
}
}
//混合灰度图和分水岭效果图并显示最终的窗口
watershedImage = watershedImage * 0.5 + grayImage * 0.5;
Cv2.ImShow(WINDOW_NAME2, watershedImage);
maskImage.Dispose();
watershedImage.Dispose();
}
}
}
}
}
C++版本代码如下:
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
using namespace std;
#define WINDOW_NAME1 "【程序窗口】" //为窗口标题定义的宏
#define WINDOW_NAME2 "【分水岭算法效果图】" //为窗口标题定义的宏
Mat g_maskImage, g_srcImage;
Point prevPt(-1, -1);
static void ShowHelpText()
{
printf("\n\n\n\t欢迎来到【分水岭算法】示例程序~\n\n");
printf("\t请先用鼠标在图片窗口中标记出大致的区域,\n\n\t然后再按键【1】或者【SPACE】启动算法。"
"\n\n\t按键操作说明: \n\n"
"\t\t键盘按键【1】或者【SPACE】- 运行的分水岭分割算法\n"
"\t\t键盘按键【2】- 恢复原始图片\n"
"\t\t键盘按键【ESC】- 退出程序\n\n\n");
}
static void on_Mouse(int event, int x, int y, int flags, void*)
{
//处理鼠标不在窗口中的情况
if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows)
return;
//处理鼠标左键相关消息
if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON))
prevPt = Point(-1, -1);
else if (event == EVENT_LBUTTONDOWN)
prevPt = Point(x, y);
//鼠标左键按下并移动,绘制出白色线条
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
{
Point pt(x, y);
if (prevPt.x < 0)
prevPt = pt;
line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0);
line(g_srcImage, prevPt, pt, Scalar::all(255), 5, 8, 0);
prevPt = pt;
imshow(WINDOW_NAME1, g_srcImage);
}
}
int main(int argc, char** argv)
{
//【0】显示帮助文字
ShowHelpText();
//【1】载入原图
g_srcImage = imread("../images/mountain.jpg");
imshow(WINDOW_NAME1, g_srcImage);
Mat srcImage, grayImage;
g_srcImage.copyTo(srcImage);
cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY);
cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);
g_maskImage = Scalar::all(0);
//【2】设置鼠标回调函数
setMouseCallback(WINDOW_NAME1, on_Mouse, 0);
//【3】轮询按键,进行处理
while (1)
{
//获取键值
int c = waitKey(0);
//若按键键值为ESC时,退出
if ((char)c == 27)
break;
//按键键值为2时,恢复源图
if ((char)c == '2')
{
g_maskImage = Scalar::all(0);
srcImage.copyTo(g_srcImage);
imshow(WINDOW_NAME1, g_srcImage);
}
//若检测到按键值为1或者空格,则进行处理
if ((char)c == '1' || (char)c == ' ')
{
//定义一些参数
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
//寻找轮廓
findContours(g_maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
//轮廓为空时的处理
if (contours.empty() || hierarchy.size() == 0)
continue;
int compCount = hierarchy.size();
// 拷贝掩膜
Mat maskImage(g_maskImage.size(), CV_32S);
maskImage = Scalar::all(0);
// 循环绘制出轮廓
for (int index = 0; index < hierarchy.size(); index++)
drawContours(maskImage, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);
// 生成随机颜色
vector<Vec3b> colorTab;
for (int i = 0; i < compCount; i++)
{
int b = theRNG().uniform(0, 255);
int g = theRNG().uniform(0, 255);
int r = theRNG().uniform(0, 255);
colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
// 计算处理时间并输出到窗口中
watershed(srcImage, maskImage);
//双层循环,将分水岭图像遍历存入watershedImage中
Mat watershedImage(maskImage.size(), CV_8UC3);
for (int i = 0; i < maskImage.rows; i++)
for (int j = 0; j < maskImage.cols; j++)
{
int index = maskImage.at<int>(i, j);
if (index == -1)
watershedImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
else if (index <= 0 || index > compCount)
watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
else
watershedImage.at<Vec3b>(i, j) = colorTab[index - 1];
}
//imshow("1", watershedImage);
//imshow("2", grayImage);
//混合灰度图和分水岭效果图并显示最终的窗口
//watershedImage = watershedImage * 0.5 + grayImage * 0.5;
addWeighted(watershedImage, 0.5, grayImage, 0.5, 0, watershedImage);
imshow(WINDOW_NAME2, watershedImage);
}
}
return 0;
}
Python版本代码如下:
import cv2
import numpy as np
import random
WINDOW_NAME1 = "WINDOW_NAME1"
WINDOW_NAME2 = "WINDOW_NAME2"
prevPt = (-1, -1)
def onMouse(event, x, y, flags, param):
global g_maskImage, g_srcImage, prevPt
# 处理鼠标不在窗口中的情况
if x < 0 or x >= g_srcImage.shape[1] or y < 0 or y >= g_srcImage.shape[0]:
return
# 处理鼠标左键相关消息
if event == cv2.EVENT_LBUTTONUP or flags & cv2.EVENT_FLAG_LBUTTON <= 0:
prevPt = (-1, -1)
elif event == cv2.EVENT_LBUTTONDOWN:
prevPt = (x, y)
# 鼠标左键按下并移动,绘制出白色线条
elif event == cv2.EVENT_MOUSEMOVE or flags & cv2.EVENT_FLAG_LBUTTON > 0:
pt = (x, y)
if prevPt[0] < 0:
prevPt = pt
cv2.line(g_maskImage, prevPt, pt, (255, 255, 255), 5, 8, 0)
cv2.line(g_srcImage, prevPt, pt, (255, 255, 255), 5, 8, 0)
prevPt = pt
cv2.imshow(WINDOW_NAME1, g_srcImage)
def setMat(mat, value):
for w in range(mat.shape[1]):
for h in range(mat.shape[0]):
mat[h, w] = value
return mat
# 载入原图
g_srcImage = cv2.imread('../images/mountain.jpg')
cv2.imshow(WINDOW_NAME1, g_srcImage);
srcImage = g_srcImage.copy()
g_maskImage = cv2.cvtColor(g_srcImage, cv2.COLOR_BGR2GRAY)
grayImage = cv2.cvtColor(g_maskImage, cv2.COLOR_GRAY2BGR)
g_maskImage = setMat(g_maskImage, 0)
# 设置鼠标回调函数
cv2.setMouseCallback("WINDOW_NAME1", onMouse)
# 轮询按键,进行处理
while True:
c = cv2.waitKey(0)
# 若按键键值为ESC时,退出
if c == 27:
break
# 按键键值为2时,恢复源图
if c == 50:
g_maskImage = setMat(g_maskImage, 0)
g_srcImage = srcImage.copy()
cv2.imshow(WINDOW_NAME1, g_srcImage)
# 若检测到按键值为1,则进行处理
if c == 49 or c == 32:
# 寻找图像轮廓
contours, hierarchy = cv2.findContours(g_maskImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# 轮廓为空时的处理
if len(contours) == 0:
continue
# 拷贝掩膜
maskImage = np.ones(g_maskImage.shape, dtype=np.int32)
maskImage = setMat(maskImage, 0)
# 循环绘制出轮廓
for index in range(0, len(contours)):
# color = (random.randint(0,255), random.randint(0,255), random.randint(0,255)) # 不能采用随机数
cv2.drawContours(maskImage, contours, index, index + 1, -1, 8, hierarchy, cv2.INTER_MAX)
# 生成随机颜色
colorTab = []
for i in range(0, len(contours)):
b = random.randint(0, 255)
g = random.randint(0, 255)
r = random.randint(0, 255)
colorTab.append([b, g, r])
# 分水岭
maskImage = cv2.watershed(srcImage, maskImage)
# 双层循环,将分水岭图像遍历存入watershedImage中
compCount = len(contours)
watershedImage = np.ones((maskImage.shape[0], maskImage.shape[1], 3), np.uint8)
for i in range(maskImage.shape[1]):
for j in range(maskImage.shape[0]):
index = maskImage[j, i]
if index == -1:
watershedImage[j, i] = [255, 255, 255]
elif index <= 0 or index > compCount:
watershedImage[j, i] = [0, 0, 0]
else:
watershedImage[j, i] = colorTab[index - 1]
# cv2.imshow("1", watershedImage);
# cv2.imshow("2", grayImage);
# 混合灰度图和分水岭效果图并显示最终的窗口
watershedImage = cv2.addWeighted(watershedImage, 0.5, grayImage, 0.5, 0)
cv2.imshow(WINDOW_NAME2, watershedImage);