【android opencv学习笔记】Day 23: 分水岭图像分割

分水岭(Watershed)图像分割

分水岭算法是一种基于拓扑地貌的图像分割方法,通过"模拟洪水淹没"的思想,结合用户标记实现图像的精准区域划分。

本文将从原理讲解、API解析、Android完整源码实现三个维度,带你掌握这一算法的工程落地。


核心原理:洪水淹没与标记引导

1. 基础概念:把图像当"地形图"

  • 图像像素值 = 海拔高度:亮像素为高山,暗像素为山谷。
  • 目标:找到不同山谷(同质区域)之间的"分水岭线",实现区域分割。

2. 原始算法的问题:过度分割

直接对图像做分水岭会产生大量细小区域(过度分割),原因是图像噪声和纹理会形成无数微小"山谷"。

3. 改进版:标记引导分水岭(OpenCV实现)

核心思想:用户预先标记已知区域(前景/背景),算法仅在标记之间寻找分水岭,从源头避免过度分割。

  1. 标记图像 :创建一个和原图同尺寸的32位整数图像,其中:
    • 前景物体:标记为非零正整数(如1、2、3...)
    • 背景:标记为另一非零整数(如128)
    • 未知区域:标记为0
  2. 模拟淹没:算法从标记区域开始"注水",水位上升过程中,不同标记的水相遇处形成分水岭线(最终标记为-1)。
  3. 结果:图像被分割为多个带标记的同质区域,分水岭线清晰分隔不同物体。

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_8UCV_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. 算法优化

  • 预处理降噪:分割前使用高斯模糊或中值滤波去除图像噪声,减少分水岭线错误
  • 多尺度分割:对不同尺度的图像分别做分水岭,再融合结果,提升鲁棒性
  • 后处理:分割后可通过连通组件分析,去除过小的分割区域,进一步优化结果

总结

分水岭算法通过标记引导,有效解决了传统算法的过度分割问题,在物体分割、目标检测、医学影像分析等场景中广泛应用。核心在于标记图像的生成质量,合理的前景/背景标记是分割成功的关键。

相关推荐
百万小涵1 小时前
机器人ros学习--机器人平台设计
学习·机器人
ch_ziyuan1 小时前
跨平台APP封装分发系统搭建:iOS免签+安卓防报毒+IPA签名一体化
android·ios
愈努力俞幸运1 小时前
python 三引号
android·开发语言·python
2301_809051141 小时前
Linux IO模型与并发服务器 学习笔记
笔记·学习
nashane1 小时前
HarmonyOS 6学习:麦克风“抢戏”打断音频?AudioSession焦点避坑指南
学习·音视频·harmonyos
恋猫de小郭1 小时前
AI 时代,谷歌都在 Android 官方做了哪些支持?
android·前端·flutter
半导体守望者1 小时前
MKS RPS AX7657-85 故障分析与可能解决方案
学习·机器人·自动化·制造·模块测试
游戏开发爱好者82 小时前
React Grab工具详解:AI助力Vue3、Svelte和Solid前端元素调试
android·ios·小程序·https·uni-app·iphone·webview
黄林晴2 小时前
Android 性能新利器!APA 公开测试版上线
android·性能优化