图像形态学
一、像素的距离
图像中像素之间的距离有多种度量方式,其中常用的有欧式距离、棋盘距离和街区距离。
1. 欧式距离:
欧式距离是指连接两个点的直线距离。欧式距离用数学公式可以表达如下:
//勾股定理计算
d^2 = (x1-x2)^2+(y1-y2)^2
2. 棋盘距离:
棋盘距离也叫切比雪夫距离,棋盘距离可以用国际象棋中王的走法来说明,也就是王从一个位置走到另一个位置最少需要多少步。
例如,王从b3走到g6最少需要几步?即b3->c4->d5->e6->f6->g6,共5步。不难发现,起始位置和终止位置在棋盘上的距离是,横向5格,纵向3格,所以棋盘距离实际上是横向距离与纵向距离之间的较大者,用数学公式表达如下:
//两个向量在任意坐标维度上的最大差值
d = max(|x1-x2|, |y1-y2|)
3. 街区距离:
街区距离也称为曼哈度距离。与棋盘距离不同,在街区距离中只允许横向或纵向移动,不允许斜向移动。那么,上例中王的两个位置即为8步。用数据公式表达如下:
d = |x1-x2| + |y1-y2|
//计算一张图像中非零像素到最近的零像素的距离,即到零像素的最短距离
void Imgproc.distanceTransform(Mat src, Mat dst, int dstanceType, int maskSize)
- src:输入图像,必须为8位单通道
- dst:含计算距离的输出图像,图像深度为CV_8U或CV_32F的单通道图像,尺寸与输入图像相同。
- distanceType:距离类型,可选参数如下:
- Imgproc.DIST_USER:用户自定义距离
- Imgproc.DIST_L1:街区距离
- Imgproc.DIST_L2:欧式距离
- Imgproc.DIST_C:棋盘距离
- Imgproc.DIST_L12:d=2(sqrt(1+x*x/2)-1)
- Imgproc.DIST_FAIR:d=c^2(|x|/c-log(1+|x|/c)),其中c=1.3998
- Imgproc.DIST_WELSCH:d=c^2/2(1-exp(-(x/c)^2)),其中c=2.9846
- Imgproc.DIST_HUBER:d=|x|<c ? x^2/2 : c(|x|-c/2),其中c=1.345
- maskSize:距离变化掩码矩阵尺寸,可选参数如下
- Imgproc.DIST_MASK_3:数字值=3
- Imgproc.DIST_MASK_5:数字值=5
- Imgproc.DIST_MASK_PRECISE
为了使用加速算法,掩码矩阵必须是对称的。在计算欧氏距离时,掩码矩阵为3时只是粗略计算像素距离(水平和垂直方向的变化量为1,对角线方向的变化量为1.4)。
public class DistanceTransform {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//构建一个5*5的小矩阵并导入数据
Mat src = new Mat(5, 5, CvType.CV_8UC1);
byte[] data = new byte[] {
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 0, 1, 1,
1, 1, 1, 1, 1,
1, 1,1 ,1 , 1
};
src.put(0, 0, data);
//打印矩阵数据
System.out.println("输入矩阵:");
System.out.println(src.dump());
System.out.println();
//计算棋盘距离并输出
Mat dst = new Mat();
Imgproc.distanceTransform(src, dst, Imgproc.DIST_C, 3);
System.out.println("棋盘距离:");
System.out.println(dst.dump());
System.out.println();
//计算街区距离并输出
Imgproc.distanceTransform(src, dst, Imgproc.DIST_L1, 3);
System.out.println("街区距离:");
System.out.println(dst.dump());
System.out.println();
//计算欧氏距离并输出
Imgproc.distanceTransform(src, dst, Imgproc.DIST_L2, 3);
System.out.println("欧氏距离:");
System.out.println(dst.dump());
System.out.println();
}
}
输入矩阵:
[ 1, 1, 1, 1, 1;
1, 1, 1, 1, 1;
1, 1, 0, 1, 1;
1, 1, 1, 1, 1;
1, 1, 1, 1, 1]
棋盘距离:
[2, 2, 2, 2, 2;
2, 1, 1, 1, 2;
2, 1, 0, 1, 2;
2, 1, 1, 1, 2;
2, 2, 2, 2, 2]
街区距离:
[4, 3, 2, 3, 4;
3, 2, 1, 2, 3;
2, 1, 0, 1, 2;
3, 2, 1, 2, 3;
4, 3, 2, 3, 4]
欧氏距离:
[2.7385864, 2.324295, 1.9100037, 2.324295, 2.7385864;
2.324295, 1.3692932, 0.95500183, 1.3692932, 2.324295;
1.9100037, 0.95500183, 0, 0.95500183, 1.9100037;
2.324295, 1.3692932, 0.95500183, 1.3692932, 2.324295;
2.7385864, 2.324295, 1.9100037, 2.324295, 2.7385864]
上面的程序仅用于说明像素距离的计算方法。利用计算出的距离可以实现很多功能。下面示例说明如何用像素距离实现轮廓的细化:
public class Thinning {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像为灰度图
Mat image = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/wang.png");
Mat gray = new Mat();
Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);
//将灰度图反相并显示
Core.bitwise_not(gray, gray);
HighGui.imshow("gray", gray);
HighGui.waitKey(0);
//进行高斯滤波和二值化处理
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 2);
Imgproc.threshold(gray, gray, 20, 255, Imgproc.THRESH_BINARY);
HighGui.imshow("Binary", gray);
HighGui.waitKey(0);
//计算街区距离
Mat thin = new Mat(gray.size(), CvType.CV_32FC1);
Imgproc.distanceTransform(gray, thin, Imgproc.DIST_L1, 3);
//获取最大的街区距离
float max = 0;
for (int i = 0; i < thin.rows(); i++) {
for (int j = 0; j < thin.cols(); j++) {
float[] f = new float[3];
thin.get(i, j, f);
if (f[0] > max) {
max = f[0];
}
}
}
//定义用于显示结果的矩阵,背景为全黑
Mat show = Mat.zeros(gray.size(), CvType.CV_8UC1);
//将距离符合一定条件的像素设为白色
for (int i = 0; i < thin.rows(); i++) {
for (int j = 0; j < thin.cols(); j++) {
float[] f = new float[3];
thin.get(i, j, f);
if (f[0] > max / 3) {
show.put(i, j, 255);
}
}
}
//在屏幕上显示最后结果
HighGui.imshow("thin", show);
HighGui.waitKey(0);
System.exit(0);
}
}
原图灰度图反相:
二值化:
通过计算街区距离,细化为原来的1/3:
二、像素的邻域
像素的邻域是指与某一像素相邻的像素集合。邻域通常分为4邻域、8邻域和D邻域,如下图:
//4邻域,当前像素为中心,上、下、左、右4个像素
1
1 P 1
1
//8邻域,当前像素为中心,上、下、左、右、左上、左下、右上、右下8个像素
1 1 1
1 P 1
1 1 1
//D邻域,当前像素为中心,左上、左下、右上、右下4个像素
1 1
P
1 1
8邻域=4邻域+D邻域,OpenCV中有如下函数标记图像中的连通域:
int Imgproc.connectedComponents(Mat image, Mat labels, int connectivity, int ltype)
- image:需要标记的8位单通道图像
- labels:标记不同连通域后的输出图像
- connectivity:标记连通域时的邻域种类,8代表8邻域,4代表4邻域
- ltype:输出图像的标签类型,目前支持CV_32S和CV_16U
- int返回值:连通域个数
public class ConnectedComponents {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo2/seed.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
//将图像转位灰度图并二值化
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
Core.bitwise_not(gray, gray);//反相操作
Mat binary = new Mat();
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
//在屏幕显示二值图
HighGui.imshow("Binary", binary);
HighGui.waitKey(0);
//标记连通域
Mat labels = new Mat(src.size(), CvType.CV_32S);
//num为连通域个数
int num = Imgproc.connectedComponents(binary, labels, 8, CvType.CV_32S);
//定义颜色数组,用于不同的连通域
Scalar[] colors = new Scalar[num];
//随机生成颜色
Random rd = new Random();
for (int i = 0; i < num; i++) {
int r = rd.nextInt(256);
int g = rd.nextInt(256);
int b = rd.nextInt(256);
colors[i] = new Scalar(r, g, b);
}
//标记各连通域,dst为用于标记的图像
Mat dst = new Mat(src.size(), src.type(), new Scalar(255, 255, 255));
int width = src.cols();
int height = src.rows();
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
//获取标签号
int label = (int)labels.get(i, j)[0];
//黑色背景色不变
if (label == 0) {
continue;
}
//根据标签号设置颜色
double[] val = new double[3];
val[0] = colors[label].val[0];
val[1] = colors[label].val[1];
val[2] = colors[label].val[2];
dst.put(i, j, val);
}
}
//在屏幕上显示最后结果
HighGui.imshow("labelled", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
二值图:
用不同颜色标记后的连通域:
//标记图像中的连通域,并输出统计信息
int Imgproc.connectedComponentsWithStats(Mat image, Mat labels, Mat stats, Mat centroids)
- image:需要标记的8位单通道图像
- labels:标记不同连通域后的输出图像
- stats:每个标签的统计信息输出,含背景标签,数据类型为CV_32S
- centroids:每个连通域的质心坐标,数据类型为CV_64F
public class ConnectedComponentsWithStats {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo2/seed.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
//将图像转位灰度图并二值化
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
Core.bitwise_not(gray, gray);//反相操作
Mat binary = new Mat();
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
//在屏幕显示二值图
HighGui.imshow("Binary", binary);
HighGui.waitKey(0);
//标记连通域
Mat labels = new Mat(src.size(), CvType.CV_32S);
Mat stats = new Mat();
Mat centroids = new Mat();
int num = Imgproc.connectedComponentsWithStats(binary, labels, stats, centroids);
//定义颜色数组,用于不同的连通域
Scalar[] colors = new Scalar[num];
//随机生成颜色
Random rd = new Random();
for (int i = 0; i < num; i++) {
int r = rd.nextInt(256);
int g = rd.nextInt(256);
int b = rd.nextInt(256);
colors[i] = new Scalar(r, g, b);
}
//标记各连通域,dst为用于标记的图像
Mat dst = new Mat(src.size(), src.type(), new Scalar(255, 255, 255));
int width = src.cols();
int height = src.rows();
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
//获取标签号
int label = (int)labels.get(i, j)[0];
//将背景以外的连通域设为黑色
if (label != 0) {
double[] val = new double[]{0, 0, 0};
dst.put(i, j, val);
}
}
}
//绘制各连通域的质心和外接矩形
for (int i = 1; i < num; i++) {
//获取连通域中心位置
double cx = centroids.get(i, 0)[0];
double cy = centroids.get(i, 1)[0];
int left = (int)stats.get(i, Imgproc.CC_STAT_LEFT)[0];
int top = (int)stats.get(i, Imgproc.CC_STAT_TOP)[0];
width = (int)stats.get(i, Imgproc.CC_STAT_WIDTH)[0];
height = (int)stats.get(i, Imgproc.CC_STAT_HEIGHT)[0];
//绘制连通域质心
Imgproc.circle(dst, new Point(cx, cy), 2, new Scalar(0, 0, 255), 2, 8, 0);
//绘制连通域外接矩形
Imgproc.rectangle(dst, new Point(left, top), new Point(left + width, top + height), colors[i], 2, 8, 0);
}
//在屏幕上显示最后结果
HighGui.imshow("labelled", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
二值化:
标记质心和外接矩形的连通域图:
三、膨胀与腐蚀
3.1、结构元素
腐蚀和膨胀是形态学中最基本的操作,其他的形态学操作,如开运算、闭运算、顶帽、黑帽等,本质上都是腐蚀和膨胀的组合运算。形态学一般需要两个输入参数,一个是用于操作的图像,另一个是类似卷积核的元素,称为结构元素。结构元素还有一个用于定位的参考点,称为锚点。
//根据指定的尺寸和形状生成形态学操作的结构元素
Mat Imgproc.getStructuringElement(int shape, Size ksize, Point anchor)
- shape:结构元素的形状,有下列选项:
- Imgproc.MORPH_RECT:矩形结构元素
- Imgproc.MORPH_CROSS:十字型结构元素
- Imgproc.MORPH_ELLIPSE:椭圆结构元素,矩形的内接椭圆
- ksize:结构元素的尺寸
- anchor:结构元素内的锚点,默认值(-1, -1),表示锚点位于结构元素中心。只有十字型的结构元素的形状取决于锚点的位置。其他情况仅仅用于调节形态学操作结果的平移量。
public class StructureElement {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//矩形结构元素
Mat k0 = Imgproc.getStructuringElement(0, new Size(3, 3));
System.out.println(k0.dump());
System.out.println();
//十字型结构元素
Mat k1 = Imgproc.getStructuringElement(1, new Size(3, 3));
System.out.println(k1.dump());
System.out.println();
//椭圆结构元素
Mat k2 = Imgproc.getStructuringElement(2, new Size(7, 7));
System.out.println(k2.dump());
System.out.println();
}
}
[ 1, 1, 1;
1, 1, 1;
1, 1, 1]
[ 0, 1, 0;
1, 1, 1;
0, 1, 0]
[ 0, 0, 0, 1, 0, 0, 0;
0, 1, 1, 1, 1, 1, 0;
1, 1, 1, 1, 1, 1, 1;
1, 1, 1, 1, 1, 1, 1;
1, 1, 1, 1, 1, 1, 1;
0, 1, 1, 1, 1, 1, 0;
0, 0, 0, 1, 0, 0, 0]
3.2、腐蚀
腐蚀是求局部最小值的操作。经过腐蚀操作后,图像中的高亮区域会缩小,就像被腐蚀了一样。
腐蚀运算的原理如下图,原图像标有1的像素为高亮区域,结构元素中心的像素为锚点。腐蚀操作时用结构元素扫描原图像,用结构元素与覆盖区域的像素进行与运算,如果所有像素的运算结果都是1,则该像素值为1,否则为0。
//原图像
0 0 0 0 0 0
0 0 1 1 1 0
0 1 1 1 1 0
0 1 1 1 1 0
0 1 1 0 0 0
0 0 1 1 0 0
//结构元素
0 1 0
1 1 1
0 1 0
//腐蚀运算结果
0 0 0 0 0 0
0 0 0 0 0 0
0 0 1 1 0 0
0 0 1 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
可以发现,原图像中高亮区域有15像素,经过腐蚀操作后只有3像素了,所以的结构相当于给高亮区域瘦身,瘦身的效果取决于结构元素,而结构元素可以根据需求自行定义。需要注意的是,腐蚀操作及膨胀操作等形态学操作都是针对高亮区域。如果原图像是黑白二值图像,则被腐蚀的是白色区域,如果希望黑色区域被腐蚀,则可以在操作前先进行反向操作。
//用特定的结构元素对图像进行腐蚀操作
void Imgproc.erode(Mat src, Mat dst, Mat kernel, Point anchor, int iterations)
- src:输入图像,通道数任意,但深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F
- dst:输出图像,和src具有相同的尺寸和数据类型
- kernel:用于腐蚀操作的结构元素,可以用getStructuringElement()函数生成
- anchor:结构元素内锚点的位置,默认(-1, -1),表示锚点位于结构元素中心
- iterations:腐蚀的次数
此方法可以设置迭代次数,即一次完成多次腐蚀操作。如果只需腐蚀一次,则可将后两个参数省略。
public class Erode {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo/wang.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
//将图像反向并显示
Core.bitwise_not(src, src);
HighGui.imshow("negative", src);
HighGui.waitKey(0);
//生成十字型结构元素
Mat kernel = Imgproc.getStructuringElement(1, new Size(3, 3));
Point anchor = new Point(-1, -1);
//腐蚀操作1次并显示
Mat dst = new Mat();
Imgproc.erode(src, dst, new Mat());
HighGui.imshow("erode=1", dst);
HighGui.waitKey(0);
//腐蚀操作3次并显示
Imgproc.erode(src, dst, kernel, anchor, 3);
HighGui.imshow("erode=3", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
反向:
一次腐蚀:
三次腐蚀:
上面的示例只能看到腐蚀的结构,无法对腐蚀的细节进行研究。下面用一个完整的程序说明:
public class Erode2 {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//构建一个6*6的小矩阵并导入数据
Mat src = new Mat(6, 6, CvType.CV_8UC1);
byte[] data = new byte[] {
0,0,0,0,0,0,
0,0,1,1,1,0,
0,1,1,1,1,0,
0,1,1,1,1,0,
0,1,1,0,0,0,
0,0,1,1,0,0
};
src.put(0, 0, data);
//打印矩阵
System.out.println(src.dump());
System.out.println();
//构建十字型结构元素
Mat kernel = Imgproc.getStructuringElement(1, new Size(3, 3));
Mat dst = new Mat(6, 6, CvType.CV_8UC1);
//进行腐蚀操作并输出腐蚀后的矩阵
Imgproc.erode(src, dst, kernel);
System.out.println(dst.dump());
}
}
[ 0, 0, 0, 0, 0, 0;
0, 0, 1, 1, 1, 0;
0, 1, 1, 1, 1, 0;
0, 1, 1, 1, 1, 0;
0, 1, 1, 0, 0, 0;
0, 0, 1, 1, 0, 0]
[ 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0;
0, 0, 1, 1, 0, 0;
0, 0, 1, 0, 0, 0;
0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0]
3.3、膨胀
与腐蚀相反,膨胀则是求局部最大值的操作。经过膨胀操作后,图像中的高亮区域会扩大,就像受热膨胀一样。膨胀运算原理如下,原图像中标有1的像素为高亮区域,结构元素中心的像素为锚点。进行膨胀操作时用结构元素扫描原图像,用结构元素与覆盖区域的像素进行与运算,如果所有像素的运算结构都是0,则该像素值为0,否则为1。膨胀运算前高亮区域有15像素,经过膨胀操作后扩充为29像素,所以膨胀的结果让高亮区域长胖了。
//原图像
0 0 0 0 0 0
0 0 1 1 1 0
0 1 1 1 1 0
0 1 1 1 1 0
0 1 1 0 0 0
0 0 1 1 0 0
//结构元素
0 1 0
1 1 1
0 1 0
//膨胀运算结果
0 0 1 1 1 0
0 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 0
0 1 1 1 1 0
//用特定的结构元素对图像进行膨胀操作
void Imgproc.dilate(Mat src, Mat dst, Mat kernel, Point anchor, int iterations)
- src:输入图像,通道数任意,但深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F
- dst:输出图像,和src具有相同的尺寸和数据类型
- kernel:用于膨胀操作的结构元素,可以用getStructuringElement()函数生成
- anchor:结构元素内锚点的位置,默认(-1, -1),表示锚点位于结构元素中心
- iterations:膨胀的次数
public class Dilate {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo/wang.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
//将图像反向并显示
Core.bitwise_not(src, src);
HighGui.imshow("negative", src);
HighGui.waitKey(0);
//生成十字型结构元素
Mat kernel = Imgproc.getStructuringElement(1, new Size(3, 3));
Point anchor = new Point(-1, -1);
//膨胀操作1次并显示
Mat dst = new Mat();
Imgproc.dilate(src, dst, new Mat());
HighGui.imshow("dilate=1", dst);
HighGui.waitKey(0);
//腐蚀操作3次并显示
Imgproc.dilate(src, dst, kernel, anchor, 3);
HighGui.imshow("dilate=3", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
反向:
膨胀一次:
膨胀三次:
四、形态学操作
腐蚀和膨胀操作时图像形态学的基础。通过对腐蚀和膨胀操作进行不同的组合可以实现图像的开运算、闭运算、形态学梯度、顶帽运算、黑帽运算和击中击不中等操作。
这些操作在OpenCV中都使用morphologyEx()函数实现,只是其中的参数不同。该函数如下:
//对图像进行基于腐蚀和膨胀的高级形态学操作
void Imgproc.morphologyEx(Mat src, Mat dst, int op, Mat kernel, Point anchor, int iterations)
- src:输入图像,通道数任意,但深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F
- dst:输出图像,和src具有相同的尺寸和数据类型
- op:形态学操作的类型,具体有以下几种:
- Imgproc.MORPH_ERODE:腐蚀操作
- Imgproc.MORPH_DILATE:膨胀操作
- Imgproc.MORPH_OPEN:开运算
- Imgproc.MORPH_CLOSE:闭运算
- Imgproc.MORPH_GRADIENT:形态学梯度
- Imgproc.MORPH_TOPHAT:顶帽运算
- Imgproc.MORPH_BLACKHAT:黑帽运算
- Imgproc.MORPH_HITMISS:击中击不中。只支持CV_8UC1类型的二值图像
- kernel:结构元素,可以用getStructuringElement函数生成
- anchor:结构元素内锚点的位置,负数表示锚点位于结构元素中心
- iterations:腐蚀和膨胀的次数
4.1、开运算和闭运算
开运算是对图像先腐蚀后膨胀的过程,它可以用来去除噪声、去除细小的形状(如毛刺)或在轻微连接处分离物体等。腐蚀操作同样能去掉毛刺,但是腐蚀操作后高亮区域整个廋了一圈,形态发生了明显变化,而开运算能在去掉毛刺的同时又保持原来的大小。
与开运算相反的操作是闭预算。闭运算是对图像先膨胀后腐蚀的过程。闭算远可以去除小型空洞,还能将狭窄的缺口连接起来。
public class MorphologyEx1 {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly2.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
Mat dst = new Mat();
//闭运算1次并显示
Point anchor = new Point(-1, -1);
Imgproc.morphologyEx(src, dst, Imgproc.MORPH_CLOSE, new Mat(), anchor, 1);
HighGui.imshow("Close-1", dst);
HighGui.waitKey(0);
//闭运算3次并显示
Imgproc.morphologyEx(src, dst, Imgproc.MORPH_CLOSE, new Mat(), anchor, 3);
HighGui.imshow("Close-3", dst);
HighGui.waitKey(0);
//在3次闭运算的基础上进行开运算并显示
Imgproc.morphologyEx(dst, dst, Imgproc.MORPH_OPEN, new Mat(), anchor, 1);
HighGui.imshow("Open", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
闭运算1次:
闭运算3次:
开运算一次:
输入图像是一只蝴蝶的轮廓图,经过1次闭运算后部分空洞消失,3次闭运算后大量空洞消失,在此基础上进行1次开运算使很多轮廓线消失。
4.2、顶帽和黑帽
顶帽运算也称为礼貌运算,是计算原图像与开运算结果之差的操作。由于开运算后放大了裂缝或者局部低亮度的区域,从原图中减去开运算后的图像后就突出了比原图像轮廓周边区域更明亮的区域。
黑帽运算则是计算闭运算结果与原图像之差的操作。黑帽运算后突出了比原图像轮廓周边区域更暗的区域。
public class MorphologyEx2 {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/shaomai.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
Mat dst = new Mat();
//转换为二值图并显示
Imgproc.threshold(src, src, 120, 255, Imgproc.THRESH_BINARY);
HighGui.imshow("Binary", src);
HighGui.waitKey(0);
//顶帽运算3次并显示
Point anchor = new Point(-1, -1);
Imgproc.morphologyEx(src, dst, Imgproc.MORPH_TOPHAT, new Mat(), anchor, 3);
HighGui.imshow("Tophat", dst);
HighGui.waitKey(0);
//黑帽运算3次并显示
Imgproc.morphologyEx(src, dst, Imgproc.MORPH_BLACKHAT, new Mat(), anchor, 3);
HighGui.imshow("Blackhat", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
二值图:
顶帽3次:
黑帽3次:
4.3、形态学梯度
形态学梯度是计算膨胀结果与腐蚀结果之差的操作,其结果看上去就像图像的轮廓。
public class MorphologyEx3 {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像1并显示
Mat src1 = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/shaomai.png");
HighGui.imshow("src1", src1);
HighGui.waitKey(0);
//形态学梯度并显示
Point anchor = new Point(-1, -1);
Mat dst = new Mat();
Imgproc.morphologyEx(src1, dst, Imgproc.MORPH_GRADIENT, new Mat(), anchor, 1);
HighGui.imshow("Gradient1", dst);
HighGui.waitKey(0);
//读取图像2并显示
Mat src2 = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/wang.png", Imgcodecs.IMREAD_GRAYSCALE);
HighGui.imshow("src2", src2);
HighGui.waitKey(0);
//形态学梯度并显示
Imgproc.morphologyEx(src2, dst, Imgproc.MORPH_GRADIENT, new Mat(), anchor, 1);
HighGui.imshow("Gradient2", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图1:
形态学梯度:
原图2:
形态学梯度:
4.4、击中击不中
击中击不中运算常用于二值图像,它的要求比腐蚀操作还要严格。只有当结构元素与其覆盖的区域完全相同时,该像素才为1,否则为0,如下:
//原图
0 0 0 0 0 0
0 1 1 1 1 0
0 1 1 0 1 0
0 1 1 1 1 1
0 0 0 0 0 0
//结构元素
1 1 1
1 0 1
1 1 1
//击中击不中结果
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
public class HitMiss {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//构建一个6*6的小矩阵并导入数据
Mat src = new Mat(6, 6, CvType.CV_8UC1);
byte[] data = new byte[] {
0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0,
0, 1, 1, 1, 1, 0,
0, 1, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0,
0, 0, 1, 1, 0, 0
};
src.put(0, 0, data);
//打印矩阵输出
System.out.println(src.dump());
System.out.println();
//构建矩形结构元素并输出
Mat kernel = Imgproc.getStructuringElement(0, new Size(3, 3));
System.out.println(kernel.dump());
System.out.println();
//击中击不中测试并输出
Point anchor = new Point(-1, -1);
Mat dst = new Mat(6, 6, CvType.CV_8UC1);
Imgproc.morphologyEx(src, dst, Imgproc.MORPH_HITMISS, kernel, anchor, 1);
System.out.println(dst.dump());
}
}
[ 0, 0, 0, 0, 0, 0;
0, 0, 1, 1, 1, 0;
0, 1, 1, 1, 1, 0;
0, 1, 1, 1, 1, 0;
0, 1, 1, 0, 0, 0;
0, 0, 1, 1, 0, 0]
[ 1, 1, 1;
1, 1, 1;
1, 1, 1]
[ 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0;
0, 0, 0, 1, 0, 0;
0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0]