【android opencv学习笔记】Day 20: 形态学滤波的腐蚀与膨胀

形态学滤波------腐蚀与膨胀详解

形态学滤波是图像处理的基础技术,而**腐蚀(Erosion)膨胀(Dilation)**是形态学运算中最核心、最基础的两个操作。

它们是后续所有高级形态学运算(开运算、闭运算、梯度、顶帽、黑帽)的基石,在图像去噪、边缘检测、目标提取等场景中应用极广。

本文将从数学原理、直观理解、代码实现三个维度,带你彻底掌握腐蚀与膨胀,并提供可直接运行的 Android OpenCV 完整源码。


核心原理

1. 什么是结构元素?

形态学运算的核心是结构元素(Structuring Element),它可以理解为一个"探测模板":

  • 通常是一个正方形、圆形或菱形的矩阵(如 3×3、5×5、7×7)
  • 矩阵中的非零元素代表结构元素的有效区域
  • 有一个锚点(通常为中心),用于对齐图像中的像素点
  • 运算时,用这个模板在图像上滑动,对每个像素进行处理

2. 腐蚀(Erosion):"瘦身"与去噪

定义:腐蚀是求局部最小值的操作。

  • 原理:将结构元素锚点对齐到图像的每个像素,遍历结构元素覆盖的所有像素,取最小值作为锚点位置的新像素值。
  • 对于二值图像(背景黑 0,前景白 255):
    • 若结构元素覆盖的区域内有任何一个像素为 0(背景),锚点位置就被腐蚀为 0(背景)
    • 效果:前景物体被"瘦身",边缘被侵蚀,细小的噪点被直接消除
  • 直观理解:腐蚀就像"吃"掉前景物体的边缘,同时吃掉背景中的小白点噪声

3. 膨胀(Dilation):"增肥"与填充

定义:膨胀是求局部最大值的操作。

  • 原理:将结构元素锚点对齐到图像的每个像素,遍历结构元素覆盖的所有像素,取最大值作为锚点位置的新像素值。
  • 对于二值图像:
    • 若结构元素覆盖的区域内有任何一个像素为 255(前景),锚点位置就被膨胀为 255(前景)
    • 效果:前景物体被"增肥",边缘被扩张,物体内部的小黑洞被填充
  • 直观理解:膨胀就像"长"大前景物体的边缘,同时填充物体内部的小孔洞

4. 关键性质与注意事项

  • 腐蚀与膨胀是对偶运算:对前景物体的腐蚀,等价于对背景的膨胀;反之亦然。
  • 重复运算:使用大尺寸结构元素,等价于多次使用小尺寸结构元素(如 7×7 结构元素腐蚀一次 ≈ 3×3 结构元素腐蚀三次)。
  • 噪声处理:腐蚀可去除前景外的白色噪点,膨胀可去除前景内的黑色孔洞,两者常组合使用(开运算/闭运算)。

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_eroded"
        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_dilated"
        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

负责图片加载、调用 C++ 算法、结果展示:

kotlin 复制代码
package com.nicoli.hellomorphology

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 接口声明
    // 1. 二值化预处理(腐蚀/膨胀需要二值图)
    private external fun binarizeImage(src: Bitmap, out: Bitmap, thresh: Int)
    // 2. 腐蚀操作
    private external fun erodeImage(src: Bitmap, out: Bitmap, kernelSize: Int, iterations: Int)
    // 3. 膨胀操作
    private external fun dilateImage(src: Bitmap, out: Bitmap, kernelSize: Int, iterations: Int)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 1. 加载原图(黑白或灰度图)
        val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.binary_input)

        // 2. 创建输出位图
        val binaryBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
        val erodedBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
        val dilatedBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)

        // 3. 执行算法流程
        binarizeImage(originalBitmap, binaryBitmap, 127) // 二值化
        erodeImage(binaryBitmap, erodedBitmap, 3, 1)    // 3×3 结构元素,腐蚀1次
        dilateImage(binaryBitmap, dilatedBitmap, 3, 1)  // 3×3 结构元素,膨胀1次

        // 4. 显示结果
        findViewById<ImageView>(R.id.iv_original).setImageBitmap(originalBitmap)
        findViewById<ImageView>(R.id.iv_eroded).setImageBitmap(erodedBitmap)
        findViewById<ImageView>(R.id.iv_dilated).setImageBitmap(dilatedBitmap)
    }
}

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 gray;
    cvtColor(rgba, gray, COLOR_RGBA2GRAY); // 转为灰度图
    AndroidBitmap_unlockPixels(env, bitmap);
    return gray;
}

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;
    cvtColor(srcMat, rgba, COLOR_GRAY2RGBA); // 转为 RGBA
    memcpy(pixels, rgba.data, info.width * info.height * 4);
    AndroidBitmap_unlockPixels(env, dstBitmap);
}

// ====================== 预处理:图像二值化 ======================
// 腐蚀/膨胀通常作用于二值图像,因此先对灰度图做阈值化
void binarizeImpl(const Mat& src, Mat& out, int threshVal) {
    threshold(src, out, threshVal, 255, THRESH_BINARY);
}

// ====================== 核心:腐蚀操作 ======================
// 参数:kernelSize-结构元素大小(3/5/7) iterations-重复次数
void erodeImpl(const Mat& src, Mat& out, int kernelSize, int iterations) {
    // 创建结构元素:kernelSize×kernelSize 的全1矩阵(默认锚点为中心)
    Mat element = getStructuringElement(MORPH_RECT, Size(kernelSize, kernelSize));
    // 执行腐蚀
    erode(src, out, element, Point(-1, -1), iterations);
}

// ====================== 核心:膨胀操作 ======================
void dilateImpl(const Mat& src, Mat& out, int kernelSize, int iterations) {
    // 创建结构元素
    Mat element = getStructuringElement(MORPH_RECT, Size(kernelSize, kernelSize));
    // 执行膨胀
    dilate(src, out, element, Point(-1, -1), iterations);
}

// ====================== JNI 接口 ======================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_binarizeImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint thresh) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat out;
    binarizeImpl(src, out, thresh);
    matToBitmap(env, out, outBitmap);
}

extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_erodeImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint kernelSize, jint iterations) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat out;
    erodeImpl(src, out, kernelSize, iterations);
    matToBitmap(env, out, outBitmap);
}

extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_dilateImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint kernelSize, jint iterations) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat out;
    dilateImpl(src, out, kernelSize, iterations);
    matToBitmap(env, out, outBitmap);
}

效果与参数详解

1. 运行效果

  • 原图:带有噪点的二值图像(前景为白色物体,背景为黑色)
  • 腐蚀结果:白色物体边缘收缩,背景中的小白噪点被消除
  • 膨胀结果:白色物体边缘扩张,物体内部的小黑孔洞被填充

2. 关键参数说明

  • 结构元素大小(kernelSize)
    • 3×3:轻微腐蚀/膨胀,适合小噪点去除
    • 5×5/7×7:效果更明显,可消除较大噪点或填充较大孔洞
  • 重复次数(iterations)
    • 多次腐蚀等价于使用更大的结构元素,例如 kernelSize=3, iterations=3 效果 ≈ kernelSize=7, iterations=1
  • 结构元素形状
    • MORPH_RECT:矩形(默认)
    • MORPH_CROSS:十字形
    • MORPH_ELLIPSE:椭圆形

扩展与进阶应用

1. 开运算(Opening):先腐蚀后膨胀

  • 作用:去除前景外的白色噪点,同时保持物体大小基本不变
  • 实现:dilate(erode(src, element), element)

2. 闭运算(Closing):先膨胀后腐蚀

  • 作用:填充前景内的黑色孔洞,同时保持物体大小基本不变
  • 实现:erode(dilate(src, element), element)

3. 形态学梯度

  • 作用:提取物体边缘
  • 实现:dilate(src, element) - erode(src, element)

4. 灰度图像的腐蚀与膨胀

  • 腐蚀:局部最小值滤波,可去除亮噪声
  • 膨胀:局部最大值滤波,可去除暗噪声
  • 与二值图像的原理完全一致,只是操作对象为灰度值而非黑白值

总结

腐蚀与膨胀是形态学滤波的基础,掌握它们的原理与实现,就能轻松理解更复杂的形态学运算。在实际项目中,它们是图像去噪、边缘检测、目标分割的常用工具,尤其是在文本识别、工业质检、医学影像处理等场景中应用广泛。

相关推荐
ゆづき3 小时前
AI能否替代小说作家?
人工智能·笔记·学习·其他·生活
rosemary5123 小时前
推理框架负责人 — 学习路线 (inference-framework-learning-path)
学习
rosemary5123 小时前
AI Infra 后端开发工程师 — 学习路线
人工智能·学习
Hua-Jay3 小时前
OpenCV联合C++/Qt 学习笔记(二十五)----监督学习聚类及K均值聚类
c++·笔记·opencv·学习·计算机视觉·聚类
red_redemption3 小时前
自由学习记录(191)
学习
小新同学^O^3 小时前
OpenClaw 数据采集工具新手入门指南
python·学习·openclaw·纯ai文
lzp07913 小时前
基于多模态视觉模型和图文向量模型的工业图像知识库研究与应用(伍)
数据库·学习·neo4j
05候补工程师3 小时前
【考研线代】矩阵相似与对角化核心解题套路与防坑指南 (附实战笔记)
经验分享·笔记·线性代数·考研·矩阵
星恒随风3 小时前
从0开始的操作系统(3)
开发语言·笔记·学习