
形态学滤波------腐蚀与膨胀详解
形态学滤波是图像处理的基础技术,而**腐蚀(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. 灰度图像的腐蚀与膨胀
- 腐蚀:局部最小值滤波,可去除亮噪声
- 膨胀:局部最大值滤波,可去除暗噪声
- 与二值图像的原理完全一致,只是操作对象为灰度值而非黑白值
总结
腐蚀与膨胀是形态学滤波的基础,掌握它们的原理与实现,就能轻松理解更复杂的形态学运算。在实际项目中,它们是图像去噪、边缘检测、目标分割的常用工具,尤其是在文本识别、工业质检、医学影像处理等场景中应用广泛。
