
形态学开运算与闭运算详解
开运算与闭运算是腐蚀和膨胀的组合形态学运算,是图像处理中去噪、补洞、目标分割的核心工具。
本文将从原理讲解、API解析、Android完整源码实现三个维度,带你彻底掌握这两种运算。
核心原理:开运算与闭运算
1. 前置知识回顾
- 腐蚀(Erosion):邻域有黑则中心变黑,收缩前景、去除亮噪声
- 膨胀(Dilation):邻域有白则中心变白,扩张前景、填充暗孔洞
开运算与闭运算就是这两个基础操作的有序组合。
2. 开运算(Opening):先腐蚀后膨胀
定义 :开运算 = 腐蚀(Erosion) → 膨胀(Dilation)
- 操作顺序:先对图像腐蚀,再用相同结构元素膨胀
- 效果:
- 消除背景中的小白点噪声(腐蚀阶段直接吃掉无法被结构元素容纳的小物体)
- 平滑前景物体的边缘
- 不明显改变前景物体的整体大小(腐蚀后再膨胀,主体轮廓会恢复)
- 典型场景:文本识别前去除背景噪点、工业质检中过滤微小亮点缺陷
3. 闭运算(Closing):先膨胀后腐蚀
定义 :闭运算 = 膨胀(Dilation) → 腐蚀(Erosion)
- 操作顺序:先对图像膨胀,再用相同结构元素腐蚀
- 效果:
- 填充前景物体内部的小黑孔洞/裂缝(膨胀阶段先把洞填满,再腐蚀恢复轮廓)
- 连接邻近的小目标(比如断裂的文字笔画)
- 不明显改变前景物体的整体大小(膨胀后再腐蚀,主体轮廓会恢复)
- 典型场景:修复扫描文本中的断裂文字、填充目标物体的内部孔洞
4. 关键性质与对比
| 运算 | 操作顺序 | 核心作用 | 适用场景 |
|---|---|---|---|
| 开运算 | 腐蚀→膨胀 | 去亮噪、平滑边缘 | 背景噪点去除、前景物体分离 |
| 闭运算 | 膨胀→腐蚀 | 补暗洞、连接目标 | 物体内部孔洞填充、断裂目标连接 |
注意:开运算和闭运算都是幂等运算,即对同一图像重复执行相同运算,结果不会再发生变化。
OpenCV 核心 API 解析
OpenCV 提供了 cv::morphologyEx 函数,可直接实现所有高级形态学运算,无需手动组合腐蚀和膨胀。
1. 函数原型(C++)
cpp
void morphologyEx(
InputArray src, // 输入图像(单通道,灰度/二值)
OutputArray dst, // 输出图像
int op, // 形态学运算类型
InputArray kernel, // 结构元素(由getStructuringElement生成)
Point anchor = Point(-1,-1), // 锚点(默认中心)
int iterations = 1 // 重复次数(一般为1)
);
2. 关键参数 op 取值
MORPH_OPEN:开运算(先腐蚀后膨胀)MORPH_CLOSE:闭运算(先膨胀后腐蚀)MORPH_GRADIENT:形态学梯度(膨胀-腐蚀,提取边缘)MORPH_TOPHAT:顶帽运算(原图-开运算,提取亮噪声)MORPH_BLACKHAT:黑帽运算(闭运算-原图,提取暗孔洞)
3. Android/Java 版本 API
java
Imgproc.morphologyEx(
Mat src, // 输入图像
Mat dst, // 输出图像
int op, // 运算类型(如Imgproc.MORPH_OPEN)
Mat kernel // 结构元素
);
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_opened"
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_closed"
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 openImage(src: Bitmap, out: Bitmap, kernelSize: Int)
// 3. 闭运算(先膨胀后腐蚀)
private external fun closeImage(src: Bitmap, out: Bitmap, kernelSize: Int)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 加载原图(带噪点和孔洞的二值图像)
val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.noisy_binary)
// 2. 创建输出位图
val binaryBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
val openedBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
val closedBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
// 3. 执行算法流程
binarizeImage(originalBitmap, binaryBitmap, 127) // 二值化预处理
openImage(binaryBitmap, openedBitmap, 5) // 5×5 结构元素开运算
closeImage(binaryBitmap, closedBitmap, 5) // 5×5 结构元素闭运算
// 4. 显示结果
findViewById<ImageView>(R.id.iv_original).setImageBitmap(originalBitmap)
findViewById<ImageView>(R.id.iv_opened).setImageBitmap(openedBitmap)
findViewById<ImageView>(R.id.iv_closed).setImageBitmap(closedBitmap)
}
}
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);
}
// ====================== 核心:开运算(先腐蚀后膨胀) ======================
void openImpl(const Mat& src, Mat& out, int kernelSize) {
// 创建结构元素:kernelSize×kernelSize 矩形
Mat element = getStructuringElement(MORPH_RECT, Size(kernelSize, kernelSize));
// 直接调用 morphologyEx 实现开运算
morphologyEx(src, out, MORPH_OPEN, element);
}
// ====================== 核心:闭运算(先膨胀后腐蚀) ======================
void closeImpl(const Mat& src, Mat& out, int kernelSize) {
// 创建结构元素:kernelSize×kernelSize 矩形
Mat element = getStructuringElement(MORPH_RECT, Size(kernelSize, kernelSize));
// 直接调用 morphologyEx 实现闭运算
morphologyEx(src, out, MORPH_CLOSE, element);
}
// ====================== 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_openImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint kernelSize) {
Mat src = bitmapToMat(env, srcBitmap);
Mat out;
openImpl(src, out, kernelSize);
matToBitmap(env, out, outBitmap);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_closeImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint kernelSize) {
Mat src = bitmapToMat(env, srcBitmap);
Mat out;
closeImpl(src, out, kernelSize);
matToBitmap(env, out, outBitmap);
}

效果与参数详解
1. 运行效果
- 原图:带有背景小白噪点和前景内部小黑洞的二值图像
- 开运算结果:背景中的小白噪点被完全消除,前景物体边缘平滑,整体大小无明显变化
- 闭运算结果:前景物体内部的小黑洞被填充,邻近的断裂目标被连接,整体大小无明显变化
2. 关键参数说明
- 结构元素大小(kernelSize) :
- 3×3:轻微去噪/补洞,适合小瑕疵处理
- 5×5/7×7:效果更明显,可去除较大噪点或填充较大孔洞
- 结构元素形状 :
MORPH_RECT:矩形(默认,适合大多数场景)MORPH_CROSS:十字形(适合线性目标处理)MORPH_ELLIPSE:椭圆形(适合圆形目标处理)
扩展应用:形态学滤波组合使用
在实际项目中,通常会先开运算再闭运算,实现"去噪+补洞"的完整图像预处理流程:
cpp
// 先开运算去除背景噪点
morphologyEx(src, opened, MORPH_OPEN, element);
// 再闭运算填充前景孔洞
morphologyEx(opened, finalResult, MORPH_CLOSE, element);
这种组合可以同时消除背景噪声和前景瑕疵,得到干净的二值图像,为后续的轮廓检测、连通组件分析等步骤打下基础。
总结
开运算与闭运算是形态学滤波的进阶操作,它们通过组合腐蚀和膨胀,解决了单一操作无法同时处理背景噪声和前景瑕疵的问题。在文本识别、工业质检、医学影像处理等场景中,它们是图像预处理的核心工具。
