一、边缘提取常用算子
1、sobel算子边缘检测
//Sobel梯度算子
void imageSobel(){
const char* name = "lena.tif";
IplImage* image = cvLoadImage(name, CV_LOAD_IMAGE_GRAYSCALE);
if (image == NULL){
printf("image load failed.\n");
return;
}
IplImage* image3 = cvCreateImage(cvGetSize(image), image->depth, image->nChannels);
IplImage* image5 = cvCreateImage(cvGetSize(image), image->depth, image->nChannels);
int gx = 1, gy = 1;
//gx x方向上的导数阶数 gy y方向上的导数阶数 ,sobel一阶微分算子
//小核对噪声更敏感
cvSobel(image, image3, gx, gy, 3);//3*3的卷积核
cvSobel(image, image5, gx, gy, 5);//5*5的卷积核
cvNamedWindow("Origin", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Sobel3", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Sobel5", CV_WINDOW_AUTOSIZE);
cvShowImage("Origin", image);
cvShowImage("Sobel3", image3);
cvShowImage("Sobel5", image5);
cvWaitKey();
cvDestroyWindow("Origin");
cvDestroyWindow("Sobel5");
cvDestroyWindow("Sobel3");
cvReleaseImage(&image);
cvReleaseImage(&image3);
cvReleaseImage(&image5);
}
结果:默然核的大小是3(左图),右图是5*5的核,边缘要粗些。
2、拉普拉斯变换
//拉普拉斯算子
void imageLaplasi(){
const char* name = "lena.tif";//house.tif
IplImage* image = cvLoadImage(name, CV_LOAD_IMAGE_GRAYSCALE);
if (image == NULL){
printf("image load failed.\n");
return;
}
IplImage* imageG = cvCreateImage(cvGetSize(image), image->depth, image->nChannels);
IplImage* imageLOG = cvCreateImage(cvGetSize(image), image->depth, image->nChannels);
//拉普拉斯算子,二阶微分算子,对噪声比较敏感,一般不直接用于边的检测,常使用高斯拉普拉斯LOG算子
cvLaplace(image, imageG, 3);//核的大小为3
cvSmooth(image, imageLOG, CV_GAUSSIAN, 3, 3);
cvLaplace(imageLOG, imageLOG, 3);//核的大小为3
cvNamedWindow("Origin", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Laplasi", CV_WINDOW_AUTOSIZE);
cvNamedWindow("LOG", CV_WINDOW_AUTOSIZE);
cvShowImage("Origin", image);
cvShowImage("Laplasi", imageG);
cvShowImage("LOG", imageLOG);
cvWaitKey();
cvDestroyWindow("Origin");
cvDestroyWindow("Laplasi");
cvDestroyWindow("LOG");
cvReleaseImage(&image);
cvReleaseImage(&imageG);
cvReleaseImage(&imageLOG);
}
结果:结果显示先对图像进行高斯平滑,对于边缘提取的效果更好
3、Canny边缘检测
IplImage* g_img=NULL;
IplImage* g_imgDst = NULL;
const char* g_windowName = "Canny";
//通过进度条调整阈值
void CannyTrackBar(int threshold){
/*void cvCanny( const CvArr* image,CvArr* edges,double threshold1,double threshold2, int aperture_size=3 )
threshold1:阈值1, threshold2;阈值2 aperture_size:Sobel 算子大小,默认为3即表示一个3*3的矩阵
双阈值中小的用来控制边缘连接,大的用来控制强边缘的初始分割.小于下限阈值,则被抛弃
即阈值1越大,则被抛弃的像素越多。假设阈值1小于阈值2
*/
cvCanny(g_img, g_imgDst, threshold, threshold * 3, 3);
cvShowImage(g_windowName, g_imgDst);
}
//Canny边缘检测
void imageCanny(){
const char* name = "lena.tif";
IplImage* image = cvLoadImage(name, CV_LOAD_IMAGE_GRAYSCALE);
if (image == NULL){
printf("image load failed.\n");
return;
}
g_img = image;
g_imgDst = cvCreateImage(cvGetSize(image), image->depth, image->nChannels);
cvNamedWindow("Source", CV_WINDOW_AUTOSIZE);
cvShowImage("Source", image);
//在窗口中创建进度条
cvNamedWindow(g_windowName, CV_WINDOW_AUTOSIZE);
int start = 10, count = 120;
const char* barName = "threshold";
cvCreateTrackbar(barName, g_windowName, &start, count, CannyTrackBar);
cvWaitKey();
cvReleaseImage(&image);
cvDestroyWindow("Source");
cvDestroyWindow(g_windowName);
}
结果:从左至右,依次是低阈值为15,30,50的结果,可以看出,阈值越大,被抛弃的细边缘越多。
二、基于阈值的分割
1、全局阈值分割
Step1:设定阈值T,G1由大于T的像素组成,G2由小于T的像素组成
Step2:求取G1的均值m1,G2的均值m2
Step3:计算新的阈值T1 = (m1+m2)/2;
Step4:迭代,直至T1-T <=某个值。
2、OSTU法(最大类间方差法)
通过计算前景和背景的均值、方差,前景和背景的方差越大,说明图像的两部分差别越大,类间方差越大意味着错分概率越小。具体原理可在网上查阅相关资料。
OpenCV提供cvThreshold()进行阈值分割。
Step1:计算归一化的直方图,统计直方图的各个分量
Step2:阈值k,计算小于k的累积概率-P1, 和大于k的累积概率-P2
Step3:计算均值m1,m2,以及全局均值m2.
Step4:根据公式( P1(m1-mg)^2 +P2(m2-mg)^2 )计算类间方差,
Step5:迭代step2,step3,得到可分性度量(最佳阈值)。
实现代码如下:
int myOstuMethod(IplImage* image){
int width = image->width;
int height = image->height;
int pixSum = width * height;
int kValue = 0;
uchar* data = NULL;
data = (uchar*)malloc(width * height * sizeof(uchar));
data = (uchar*)image->imageData;
const int Gray = 256;
int pixCount[Gray];//各个灰度级的统计值
float pCount[Gray];//各个灰度级的出现概率
//初始化
for (int i = 0; i < Gray; i++){
pixCount[i] = 0;
pCount[i] = 0;
}
//统计各个灰度级出现的次数
for (int i = 0; i < height; i++){
for (int j = 0; j < width; j++){
pixCount[ (int)data[i*width + j] ]++;
}
}
//计算各个灰度级的概率
for (int i = 0; i < Gray; i++){
pCount[i] = (float)pixCount[i] / pixSum;
}
//计算类1和类2的类间方差和平均灰度
float m1, m2, mg, P1, P2, u1, u2, segma, segmaMax;
segmaMax = 0;
for (int i = 0; i<Gray; i++){
m1 = 0;//类一的均值
m2 = 0;//类二的均值
mg = 0;//全局均值
P1 = 0;//类一的累积概率
P2 = 0;//类二的累积概率
u1 = 0;
u2 = 0;
segma = 0;
for (int j = 0; j < Gray; j++){
//比如阈值为100,统计小于100和大于100的
if (j <= i){
P1 += pCount[j];
u1 += pCount[j] * j;
}
else{
P2 += pCount[j];
u2 += pCount[j] * j;
}
}
m1 = u1 / P1;
m2 = u2 / P2;
mg = u1 + u2;
//类间方差
segma = P1*(m1 - mg)*(m1 - mg) + P2*(m2 - mg)*(m2 - mg);
if (segma > segmaMax){
segmaMax = segma;
kValue = i;
}
//printf("segma %d :%f\n", i, segma);
}
return kValue;
}
结果:自己写的代码求得的阈值和matlab [ graythresh(I)]求得的阈值一样
下面是全局阈值和OStu法的分割实现:
/阈值分割
void imageBinaryzation(){
const char* name = "finger.tif";//finger
const char* name1 = "cell.tif";
IplImage* img = cvLoadImage(name, CV_LOAD_IMAGE_GRAYSCALE);
IplImage* img1 = cvLoadImage(name1, CV_LOAD_IMAGE_GRAYSCALE);
if (img == NULL || img1 == NULL){
printf("image load failed.\n");
return;
}
/* CV_THRESH_BINARY =0, value = value > threshold ? max_value : 0
CV_THRESH_BINARY_INV =1, value = value > threshold ? 0 : max_value
CV_THRESH_TOZERO =3, value = value > threshold ? value : 0
CV_THRESH_OTSU =8
*/
//全局阈值
IplImage* imgDst = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
double thresholdValue = 110;
double maxValue = 255;
cvThreshold(img, imgDst, thresholdValue, maxValue, CV_THRESH_BINARY);
cvNamedWindow("Source", 0);
cvNamedWindow("Binary", 0);
cvShowImage("Source", img);
cvShowImage("Binary", imgDst);
//Ostu
IplImage* imgOstu = cvCreateImage(cvGetSize(img1), IPL_DEPTH_8U, 1);
cvThreshold(img1, imgOstu, 0, maxValue, CV_THRESH_OTSU);//使用CV_THRESH_OTSU时,参数中的阈值不再起作用
cvNamedWindow("Source1", 0);
cvNamedWindow("Ostu", 0);
cvShowImage("Source1", img1);
cvShowImage("Ostu", imgOstu);
cvWaitKey();
cvDestroyWindow("Source");
cvReleaseImage(&img);
cvDestroyWindow("Binary");
cvReleaseImage(&imgDst);
}
3、自适应阈值
openCV采用cvAdaptiveThreshold()函数实现,针对有强照明或者反射梯度的图像,需要根据梯度阈值化时,自适应阈值非常有用。
自适应阈值与全局阈值的比较:
//自适应阈值分割
void imageThresholdComp(){
const char* name = "word.jpg";
IplImage* img = cvLoadImage(name, CV_LOAD_IMAGE_GRAYSCALE);
if (img == NULL){
printf("image load failed.\n");
return;
}
IplImage* imgDst1 = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* imgDst2 = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
double thresholdValue = 120;
double maxValue = 255;
cvThreshold(img, imgDst1, thresholdValue, maxValue, CV_THRESH_BINARY_INV);//CV_THRESH_BINARY_INV 像素值反转
//测试发现:相比blocksize=3, blockSize =7时字母线条更加粗些,识别性更高些
//测试CV_ADAPTIVE_THRESH_MEAN_C 效果好于 CV_ADAPTIVE_THRESH_GAUSSIAN_C,字母更加清晰连贯
int blockSize = 7;
cvAdaptiveThreshold(img, imgDst2, maxValue, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY_INV, blockSize, 5);
cvNamedWindow("Source1", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Ans1", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Ans2", CV_WINDOW_AUTOSIZE);
cvShowImage("Source1", img);
cvShowImage("Ans1", imgDst1);
cvShowImage("Ans2", imgDst2);
cvWaitKey();
}
结果:可以看出自适应阈值对于‘光照下的英文’分割得更好。