形态学
来自本人笔记: https://github.com/masterAllen/LearnOpenCV/blob/main/docs/形态学
知道形态学是干什么的,这个任何一本基本的图像处理书肯定会讲的,而且在网上搜一下实战即可;什么时候想到用形态学:涉及到极大极小都可以去往这方面想。其实形态学也可以归类为滤波。
提取确定前景、背景;涉及到轮廓等操作,都可以去想形态学。
形态学中中还有一个叫做 RunLength Encode 的技术,用于加快速度和节省空间,具体看最后的章节。
函数说明:
- getStructingElement:构造形态学的【滤波核】
- morphologyEx: 形态学的通用函数
- erode, dilate: 膨胀腐蚀的函数,可以用上面的函数替代
- distanceTransform: 距离变换函数,通常用于提取确定前景
- thinning: 细化图片,最终会得到特别细的图片
具体细节
getStructingElement
shape: 形状,有三种:矩形、十字架、椭圆;如果 shape 不满足的话,比如需要菱形,可以不用这个函数,自己构造 ndarray
morphologyEx
- 滤波核是 uint8,但本质其实是 bool,值分为 0 和 非0;输入块可以是 uint8, uint16 等,最后结果和输入块一样类型
- op: 形态学操作,包括膨胀腐蚀、开闭等等,具体请参考文档
- iterations: 遍历次数;注意这里的顺序是按照膨胀腐蚀的粒度来进行的;例子:an opening operation (MORPH_OPEN) with two iterations is equivalent to apply successively: erode -> erode -> dilate -> dilate (and not erode -> dilate -> erode -> dilate).
erode, dilate
- 膨胀腐蚀操作,完全可以被上面的函数替代(即赋值 op 为相对应的操作)
腐蚀,膨胀的概念要知道,这方面以前一直没有注意,老是觉得只是分别用来扩大黑白块的,而且网上大部分资料其实很差:要么过于学术了,用集合去定义,非常绕口;要么过于错误,如前所述,认为是扩充或减小边界的。
按照大白话解释就是:腐蚀是赋值为其在 Kernel 上所有非零位置的最小值;膨胀是赋值其在 Kernel 上所有非零位置的最大值。
s = np.array([[1,2,3], [4,5,6], [7,8,9]]).astype(np.uint8)
k = np.array([[0,1,0], [1,1,1], [0,1,0]]).astype(np.uint8)
erode(s, k)[1, 1] == 2
dilate(s, k)[1, 1] == 8
distanceTransform
建议点击链接去看 OpenCV 的说明,可以在 test_moro.ipynb 中查看具体的情况和表现。
1. 函数解释:Calculates the distance to the closest zero pixel for each pixel of the source image.
2. 函数作用:细化轮廓、提取前景(可在 test_moro.ipynb 查看具体代码)
函数详解:参数 distanceType 表示距离类型,如 DIST_L2 是欧式距离。参数 labelType 有两个 DIST_LABEL_CCOMP 或 DIS_LABEL_PIXEL,函数返回的 tuple 第一个表示标签,这个参数和标签有关。如果是 CCOMP,那么联通的零像素会被标记为一个标签;如果是 PIXEL,则每个零都会单独一个标签。
参数 maskSize 需要好好讲一下。如果是 DIST_MASK_PRECISE,这个好理解,最精确的方式;如果是 3 或者 5,则表示每一步可以走的范围:对于 3x3 而言,无非横竖走和斜着走两种;对于 5x5 而言,除了横竖和斜线,还可以走一个马步(即走日)。每一种类型有代价,比如斜着走肯定要比横竖走一步代价高,而且这个代价和什么样的距离有关。如下:
- DIST_L1: 只能 3x3 的走,横竖代价 1、斜线代价 2
- DIST_L2: 3x3 的走,横竖代价 0.955、斜线代价 1.3693;5x5 的走,横竖代价 1、斜线代价 1.4、马步代价 2.1969
- DIST_C:只能 3x3 的走,横竖代价 1、斜线代价 1
下面用例子说明,以下是 DIST_L2 的情况:
1. 对于 3x3 的 (3, 3) 点,其必须要走两步,第一步向上走、第二步向左上斜线走,这样代价为 0.955+1.3693=2.3243
2. 对于 5x5 的 (3, 3) 点,由于范围是 5x5,所以它只用走一步就到了,即走一个马步即可,这样代价为 2.1969
thinning
ximgproc 实现的函数,如名字所述,函数作用是彻底细化,最后每条粗线细化成一条线的程度。可以在 test_moro.ipynb 中看其表现情况,这里给出简单代码:
thin1 = cv2.ximgproc.thinning(binary_img, thinningType=cv2.ximgproc.THINNING_ZHANGSUEN)
thin2 = cv2.ximgproc.thinning(binary_img, thinningType=cv2.ximgproc.THINNING_GUOHALL)
Run-Length Encode
这是在 ximgproc 里面实现的一个功能,这种编码方式将连续的 "on" 像素序列组合在一个叫做 "run" 的结构中。也容易想到,每个 "run" 记录了这些连续 "on" 的第一个像素的位置和最后一个像素的位置。对于一些连续 on 或 off 的图像(如棋盘格),这种表示非常紧凑;而对于从随机噪声图像或其他相邻像素之间相关性很小的图像创建的二值图像不太适用。
对于这种表示方法,支持的形态学操作与常规模块支持的操作基本相同。通常情况下这种更快,但注意是通常,也有慢的时候。看代码即可,只能用 CPP,前面加个 rl 这个 namespace 就表示用的是 Run-Length Encode 这种编码,用法基本是一样的...
#include <iostream>
#include "opencv2/imgproc.hpp"
#include "opencv2/ximgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
using namespace std;
using namespace cv;
using namespace cv::ximgproc;
static void PaintRLEToImage(cv::Mat& rleImage, cv::Mat& res, unsigned char uValue)
{
res = cv::Scalar(0);
rl::paint(res, rleImage, Scalar((double)uValue));
}
static bool isSame(cv::Mat& image1, cv::Mat& image2)
{
cv::Mat diff;
cv::absdiff(image1, image2, diff);
int nDiff = cv::countNonZero(diff);
return (nDiff == 0);
}
int main(int argc, char** argv)
{
Mat binaryImage, dstImage;
Mat binaryRLE, dstRLE;
Mat element, elementRLE;
int64 t1, t2, t3;
for (int i = 0; i < 6; ++i) {
Mat src = imread("img" + to_string(i) + ".jpg", IMREAD_GRAYSCALE);
cout << "--------------- threshold ------------------" << endl;
t1 = getTickCount();
threshold(src, binaryImage, 127.0, 255.0, THRESH_BINARY);
t2 = getTickCount();
rl::threshold(src, binaryRLE, 127.0, THRESH_BINARY);
t3 = getTickCount();
cout << "Normal(imgproc): " << t2 - t1 << "\tRunLength(ximgproc): " << t3 - t2 << "\n" << endl;
cout << "--------------- create structing element ------------------" << endl;
t1 = getTickCount();
element = getStructuringElement(MORPH_RECT, Size(5, 5));
t2 = getTickCount();
elementRLE = rl::getStructuringElement(MORPH_RECT, Size(5, 5));
t3 = getTickCount();
cout << "Normal(imgproc): " << t2 - t1 << "\tRunLength(ximgproc): " << t3 - t2 << "\n" << endl;
cout << "-------------------- open -------------------------" << endl;
t1 = getTickCount();
morphologyEx(binaryImage, dstImage, MORPH_OPEN, element);
t2 = getTickCount();
rl::morphologyEx(binaryRLE, dstRLE, MORPH_OPEN, elementRLE, true);
t3 = getTickCount();
cout << "Normal(imgproc): " << t2 - t1 << "\tRunLength(ximgproc): " << t3 - t2 << "\n" << endl;
cout << "-------------------- erode -------------------------" << endl;
t1 = getTickCount();
erode(binaryImage, dstImage, element);
t2 = getTickCount();
rl::erode(binaryRLE, dstRLE, elementRLE, true);
t3 = getTickCount();
cout << "Normal(imgproc): " << t2 - t1 << "\tRunLength(ximgproc): " << t3 - t2 << "\n" << endl;
Mat rlePainted(dstImage.rows, dstImage.cols, dstImage.type());
// the last parma: all foreground pixel of the binary image are set to this value
PaintRLEToImage(dstRLE, rlePainted, (unsigned char)255);
cout << (isSame(dstImage, rlePainted) > 0 ? "Same" : "NotSame") << endl;
cout << endl << endl << endl;
}
}