目前的计算机图像识别,透过现象看本质,主要分为两大类:
- 基于规则运算的图像识别,例如颜色形状等模板匹配方法
- 基于统计的图像识别。例如机器学习ML,神经网络等人工智能方法
区别:模板匹配方法适合固定的场景或物体识别,机器学习方法适合大量具有共同特征的场景或物体识别。
对比:无论从识别率,准确度,还是适应多变场景变换来讲,机器学习ML都是优于模板匹配方法的;前提你有
大量的数据
来训练分类器。如果是仅仅是识别特定场景、物体或者形状,使用模板匹配方法更简单更易于实现。目标:实现在iOS客户端,通过摄像头发现并标记目标。
集成Google的TensorFlow
实现 集成OpenCV
开源计算机库来实现
- AlphaGo战胜世界围棋冠军,人工智能大火,谷歌去年开源了其用来制作AlphaGo的深度学习系统Tensorflow,而且Tensorflow支持了iOS,Android等移动端。
- OpenCV于1999年由Intel建立的,跨平台的开源计算机视觉库,主要由C和C++代码构成,有Python、Ruby、MATLAB等语言的接口,支持iOS,Android等移动设备。
- 显而易见,虽然都是开源库,都支持机器学习ML,但从推出时间,代码迭代,资料的丰富度,以及前辈已经给踩平的坑来讲,OpenCV是成熟的,应该首先选择的。
模板匹配
特征点检测
基于机器学习ML的训练分类器用来分类的方法
- 方法1: 从OpenCV官网下载opencv2.framework框架,然后拖入即可,导入依赖的库,具体集成方法见我的另外一篇文章iOS集成OpenCV
- 方法2: CocoaPods方式集成,Pod文件中配置pod ‘OpenCV’,而实验证明,用CocoaPods方式配置虽然简单,但自动配置的不正确,存在名称重复等大量的问题。
首先要做的肯定是从摄像头中获取视频帧,转为OpenCV能够用的cv::Mat矩阵,OpenCV运算是以矩阵Mat为基础的。从iPhone摄像头获取视频帧,在下方的代理方法中获取:
#pragma mark - 获取视频帧,处理视频
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
将视频帧对象高效
转换为OpenCV能够使用矩阵cv::Mat
摄像头获取的图像是偏转90度,切镜像的,通过矩阵翻转,矩阵倒置操作纠正,而且将彩色图像转换为灰度图像,加快计算速度,减少CPU占有率。
#pragma mark - 将CMSampleBufferRef转为cv::Mat
+(cv::Mat)bufferToMat:(CMSampleBufferRef) sampleBuffer{
CVImageBufferRef imgBuf = CMSampleBufferGetImageBuffer(sampleBuffer);
//锁定内存
CVPixelBufferLockBaseAddress(imgBuf, 0);
// get the address to the image data
void *imgBufAddr = CVPixelBufferGetBaseAddress(imgBuf);
// get image properties
int w = (int)CVPixelBufferGetWidth(imgBuf);
int h = (int)CVPixelBufferGetHeight(imgBuf);
// create the cv mat
cv::Mat mat(h, w, CV_8UC4, imgBufAddr, 0);
//转换为灰度图像
cv::Mat edges;
cv::cvtColor(mat, edges, CV_BGR2GRAY);
//旋转90度
cv::Mat transMat;
cv::transpose(mat, transMat);
//翻转,1是x方向,0是y方向,-1位Both
cv::Mat flipMat;
cv::flip(transMat, flipMat, 1);
CVPixelBufferUnlockBaseAddress(imgBuf, 0);
return flipMat;
}
此时已经获取了摄像头图像矩阵flipMat,下一步只需将模板图像UIImage转换为cv::Mat矩阵,提供OpenCV函数对比即可,如果上一步已经将矩阵转换为灰度图像,则cv::cvtColor(tempMat, tempMat, CV_BGR2GRAY);
这一行去掉即可。
//将图片转换为灰度的矩阵
-(cv::Mat)initTemplateImage:(NSString *)imgName{
UIImage *templateImage = [UIImage imageNamed:imgName];
cv::Mat tempMat;
UIImageToMat(templateImage, tempMat);
//cv::cvtColor(tempMat, tempMat, CV_BGR2GRAY);
return tempMat;
}
此时获取了模板UIImage的矩阵templateMat和视频帧的矩阵flipMat,只需要用OpenCV的函数对比即可。
/**
对比两个图像是否有相同区域
@return 有为Yes
*/
-(BOOL)compareInput:(cv::Mat) inputMat templateMat:(cv::Mat)tmpMat{
int result_rows = inputMat.rows - tmpMat.rows + 1;
int result_cols = inputMat.cols - tmpMat.cols + 1;
cv::Mat resultMat = cv::Mat(result_cols,result_rows,CV_32FC1);
cv::matchTemplate(inputMat, tmpMat, resultMat, cv::TM_CCOEFF_NORMED);
double minVal, maxVal;
cv::Point minLoc, maxLoc, matchLoc;
cv::minMaxLoc( resultMat, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());
// matchLoc = maxLoc;
// NSLog(@"min==%f,max==%f",minVal,maxVal);
dispatch_async(dispatch_get_main_queue(), ^{
self.similarLevelLabel.text = [NSString stringWithFormat:@"相似度:%.2f",maxVal];
});
if (maxVal > 0.7) {
//有相似位置,返回相似位置的第一个点
currentLoc = maxLoc;
return YES;
}else{
return NO;
}
}
此时,我们已经对比两个图像的相似度了,其中maxVal越大表示匹配度越高,1为完全匹配,一般想要匹配准确,需要大于0.7。
但此时我们发现一个问题,我们摄像头离图像太远或者太近,都无法识别,只有在特定的距离才能够识别。
这是因为模板匹配法,只是死板
的拿模板图像去和摄像头读取的图像进行比较,放大缩小都不行。
我们做些优化,按照图像金字塔
的方法,将模板进行动态的放大缩小,只要能够匹配,说明图像就是一样的,这样摄像头前进后退都能够识别。我们将识别出的位置和大小保存在数组中,用矩形方框来标记位置。至于怎么标记,就不细说了,方法很多。
//图像金字塔分级放大缩小匹配,最大0.8*相机图像,最小0.3*tep图像
-(NSArray *)compareByLevel:(int)level CameraInput:(cv::Mat) inputMat{
//相机输入尺寸
int inputRows = inputMat.rows;
int inputCols = inputMat.cols;
//模板的原始尺寸
int tRows = self.templateMat.rows;
int tCols = self.templateMat.cols;
NSMutableArray *marr = [NSMutableArray array];
for (int i = 0; i < level; i++) {
//取循环次数中间值
int mid = level*0.5;
//目标尺寸
cv::Size dstSize;
if (i//如果是前半个循环,先缩小处理
dstSize = cv::Size(tCols*(1-i*0.2),tRows*(1-i*0.2));
}else{
//然后再放大处理比较
int upCols = tCols*(1+i*0.2);
int upRows = tRows*(1+i*0.2);
//如果超限会崩,则做判断处理
if (upCols>=inputCols || upRows>=inputRows) {
upCols = tCols;
upRows = tRows;
}
dstSize = cv::Size(upCols,upRows);
}
//重置尺寸后的tmp图像
cv::Mat resizeMat;
cv::resize(self.templateMat, resizeMat, dstSize);
//然后比较是否相同
BOOL cmpBool = [self compareInput:inputMat templateMat:resizeMat];
if (cmpBool) {
NSLog(@"匹配缩放级别level==%d",i);
CGRect rectF = CGRectMake(currentLoc.x, currentLoc.y, dstSize.width, dstSize.height);
NSValue *rValue = [NSValue valueWithCGRect:rectF];
[marr addObject:rValue];
break;
}
}
return marr;
}
机器学习方法适合批量提取大量图片的特征,训练分类器,具体训练方法在网上查找。关键点在于训练数据比较难弄,例如人脸分类器需要的正样本人脸图像几千张,负样本需要为正样本的3倍左右。我的解决思路为从摄像头录制待识别物体,从视频帧中生成PNG格式的正样本,再拍摄不包含待识别物体的背景,仍旧从视频中自动生成PNG格式的负样本。训练级联分类器方法说明
训练完成后会生成一个XML格式的文件,我们加载这个XML文件,就可以用其来识别物体了,这里我们使用OpenCV官方库中人眼识别库haarcascade_eye_tree_eyeglasses.xml
,我们从GitHub上面下载开源库OpenCV的源代码,最新版本为3.2.0,分类器路径为/opencv-3.2.0/data目录下的文件夹中,XML格式文件就是。
加载训练好的分类器文件需要用到加载器,我们定义一个加载器属性对象
cv::CascadeClassifier icon_cascade;//分类器
加载器加载XML文件,加载成功返回YES
//加载训练文件
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"haarcascade_eye_tree_eyeglasses.xml" ofType:nil];
cv::String fileName = [bundlePath cStringUsingEncoding:NSUTF8StringEncoding];
BOOL isSuccessLoadFile = icon_cascade.load(fileName);
isSuccessLoadXml = isSuccessLoadFile;
if (isSuccessLoadFile) {
NSLog(@"Load success.......");
}else{
NSLog(@"Load failed......");
}
我们是从摄像头获取图像,仍需把视频帧转换为OpenCV能够使用的cv::Mat
矩阵格式,按照上面3.2
相同的方法转换,假设我们已经获取了视频帧转换好的灰度图像矩阵cv::Mat imgMat
,那我们用OpenCV的API接口来识别视频帧,并把识别出的位置转换为Frame存在数组中返回,我们可以随意使用这些Frame来标记识别出的位置
//获取计算出的标记的位置,保存在数组中
-(NSArray *)getTagRectInLayer:(cv::Mat) inputMat{
if (inputMat.empty()) {
return nil;
}
//图像均衡化
cv::equalizeHist(inputMat, inputMat);
//定义向量,存储识别出的位置
std::vector glassess;
//分类器识别
icon_cascade.detectMultiScale(inputMat, glassess, 1.1, 3, 0);
//转换为Frame,保存在数组中
NSMutableArray *marr = [NSMutableArray arrayWithCapacity:glassess.size()];
for (NSInteger i = 0; i < glassess.size(); i++) {
CGRect rect = CGRectMake(glassess[i].x, glassess[i].y, glassess[i].width,glassess[i].height);
NSValue *value = [NSValue valueWithCGRect:rect];
[marr addObject:value];
}
return marr.copy;
}
如果您觉得有所帮助,请在GitHub OpenCVDemo上赏个Star ⭐️,您的鼓励是我前进的动力