
分水岭(Watershed)图像分割
分水岭算法是一种基于拓扑地貌的图像分割方法,通过"模拟洪水淹没"的思想,结合用户标记实现图像的精准区域划分。
本文将从原理讲解、API解析、Android完整源码实现三个维度,带你掌握这一算法的工程落地。
核心原理:洪水淹没与标记引导
1. 基础概念:把图像当"地形图"
- 图像像素值 = 海拔高度:亮像素为高山,暗像素为山谷。
- 目标:找到不同山谷(同质区域)之间的"分水岭线",实现区域分割。
2. 原始算法的问题:过度分割
直接对图像做分水岭会产生大量细小区域(过度分割),原因是图像噪声和纹理会形成无数微小"山谷"。
3. 改进版:标记引导分水岭(OpenCV实现)
核心思想:用户预先标记已知区域(前景/背景),算法仅在标记之间寻找分水岭,从源头避免过度分割。
- 标记图像 :创建一个和原图同尺寸的32位整数图像,其中:
- 前景物体:标记为非零正整数(如1、2、3...)
- 背景:标记为另一非零整数(如128)
- 未知区域:标记为0
- 模拟淹没:算法从标记区域开始"注水",水位上升过程中,不同标记的水相遇处形成分水岭线(最终标记为-1)。
- 结果:图像被分割为多个带标记的同质区域,分水岭线清晰分隔不同物体。
OpenCV 核心 API 解析
1. 分水岭算法核心函数
cpp
void watershed(
InputArray image, // 输入:8位3通道彩色图像(CV_8UC3)
InputOutputArray markers // 输入/输出:32位单通道标记图像(CV_32SC1)
);
- 输入图像必须为彩色图(
CV_8UC3),灰度图需先转为BGR格式。 markers既是输入(用户标记),也是输出(算法修改后的分割结果):- 非零值:对应标记的区域
- -1:分水岭线(分割边界)
2. 配套预处理 API
erode/dilate:形态学操作,提纯前景/背景标记threshold:二值化,生成初始标记图像convertTo:标记图像格式转换(CV_8U→CV_32S)
Android 完整项目实现
1. 布局文件:activity_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<!-- 原图 -->
<ImageView
android:id="@+id/iv_original"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
<!-- 标记图像 -->
<ImageView
android:id="@+id/iv_markers"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="4dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
<!-- 分水岭分割结果 -->
<ImageView
android:id="@+id/iv_result"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="4dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
</LinearLayout>
2. Kotlin 上层代码:MainActivity.kt
kotlin
package com.nicoli.watersheddemo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
init {
System.loadLibrary("native-lib")
}
}
// JNI 接口声明
private external fun watershedSegment(src: Bitmap, outMarkers: Bitmap, outResult: Bitmap)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 加载原图(带前景物体的图像)
val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.animals)
// 创建输出位图
val markersBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
val resultBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
// 执行分水岭分割
watershedSegment(originalBitmap, markersBitmap, resultBitmap)
// 显示结果
findViewById<ImageView>(R.id.iv_original).setImageBitmap(originalBitmap)
findViewById<ImageView>(R.id.iv_markers).setImageBitmap(markersBitmap)
findViewById<ImageView>(R.id.iv_result).setImageBitmap(resultBitmap)
}
}
3. C++ 核心算法:native-lib.cpp(逐行注释)
cpp
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <android/bitmap.h>
using namespace cv;
using namespace std;
// ====================== 工具函数:Bitmap ↔ Mat 转换 ======================
Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
AndroidBitmapInfo info;
void* pixels;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
Mat rgba(info.height, info.width, CV_8UC4, pixels);
Mat bgr;
cvtColor(rgba, bgr, COLOR_RGBA2BGR); // 转为BGR格式(OpenCV默认)
AndroidBitmap_unlockPixels(env, bitmap);
return bgr;
}
void matToBitmap(JNIEnv *env, const Mat& srcMat, jobject dstBitmap) {
AndroidBitmapInfo info;
void* pixels;
AndroidBitmap_getInfo(env, dstBitmap, &info);
AndroidBitmap_lockPixels(env, dstBitmap, &pixels);
Mat rgba;
if (srcMat.channels() == 1) {
cvtColor(srcMat, rgba, COLOR_GRAY2RGBA);
} else {
cvtColor(srcMat, rgba, COLOR_BGR2RGBA);
}
memcpy(pixels, rgba.data, info.width * info.height * 4);
AndroidBitmap_unlockPixels(env, dstBitmap);
}
// ====================== 分水岭分割核心类 ======================
class WatershedSegmenter {
private:
Mat markers;
public:
void setMarkers(const Mat& markerImage) {
// 转换为32位有符号整数图像
markerImage.convertTo(markers, CV_32S);
}
Mat process(const Mat& image) {
// 执行分水岭算法
watershed(image, markers);
return markers;
}
// 获取分割后的标签图像
Mat getSegmentation() {
Mat tmp;
markers.convertTo(tmp, CV_8U);
return tmp;
}
// 获取分水岭线图像
Mat getWatersheds() {
Mat tmp;
// 线性变换:-1→0,非-1→255
markers.convertTo(tmp, CV_8U, 255, 255);
return tmp;
}
};
// ====================== 标记图像生成函数 ======================
Mat createMarkerImage(const Mat& srcBgr) {
Mat gray, binary;
// 转为灰度图
cvtColor(srcBgr, gray, COLOR_BGR2GRAY);
// 二值化(根据实际场景调整阈值)
threshold(gray, binary, 50, 255, THRESH_BINARY_INV);
// 1. 生成前景标记(腐蚀提纯)
Mat fg;
erode(binary, fg, Mat(), Point(-1,-1), 4);
// 2. 生成背景标记(膨胀+反向阈值)
Mat bg;
dilate(binary, bg, Mat(), Point(-1,-1), 4);
threshold(bg, bg, 1, 128, THRESH_BINARY_INV);
// 3. 合并前景和背景标记
Mat markers = Mat::zeros(binary.size(), CV_8U);
markers = fg + bg;
return markers;
}
// ====================== JNI 接口 ======================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_watersheddemo_MainActivity_watershedSegment
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outMarkers, jobject outResult) {
// 1. 转换Bitmap为OpenCV Mat
Mat srcBgr = bitmapToMat(env, srcBitmap);
// 2. 生成标记图像
Mat markers8u = createMarkerImage(srcBgr);
// 3. 执行分水岭分割
WatershedSegmenter segmenter;
segmenter.setMarkers(markers8u);
Mat result32s = segmenter.process(srcBgr);
// 4. 生成结果图像(叠加分水岭线到原图)
Mat watersheds = segmenter.getWatersheds();
Mat resultBgr = srcBgr.clone();
resultBgr.setTo(Scalar(0,0,255), watersheds == 0); // 分水岭线标记为红色
// 5. 转换结果回Bitmap
matToBitmap(env, markers8u, outMarkers);
matToBitmap(env, resultBgr, outResult);
}

效果与参数详解
1. 运行效果
- 原图:包含前景物体和背景的彩色图像
- 标记图像:前景为白色(255)、背景为灰色(128)、未知区域为黑色(0)
- 分割结果:前景物体被红色分水岭线完整包围,背景与物体清晰分离
2. 关键参数说明
- 腐蚀/膨胀迭代次数 :
- 次数越多,前景标记越纯净,但容易丢失小物体
- 次数越少,标记越接近原图,但噪声较多,易导致分割错误
- 二值化阈值:需根据图像亮度调整,确保前景物体完整分离
- 标记值设置 :
- 前景标记值(255)和背景标记值(128)可自定义,只要不相等即可
- 未知区域必须为0,算法会自动填充
扩展应用:交互式标记与改进方案
1. 交互式标记
在实际项目中,可通过用户手动点击图像设置前景/背景标记,再调用分水岭算法分割物体,实现更精准的分割效果。
2. 算法优化
- 预处理降噪:分割前使用高斯模糊或中值滤波去除图像噪声,减少分水岭线错误
- 多尺度分割:对不同尺度的图像分别做分水岭,再融合结果,提升鲁棒性
- 后处理:分割后可通过连通组件分析,去除过小的分割区域,进一步优化结果
总结
分水岭算法通过标记引导,有效解决了传统算法的过度分割问题,在物体分割、目标检测、医学影像分析等场景中广泛应用。核心在于标记图像的生成质量,合理的前景/背景标记是分割成功的关键。
