强脑
Getting start
CocoaPods
Update SDK:
$ cd BrainProject
$ pod install
Usage
- Use
Command + B
to build project. - Use
Command + R
to run project.
简介
强脑项目
整体架构
- 项目框架:使用 UIKit 框架搭建。
- 网络模块:使用针对
AFNetworking
进行再次封装实现数据请求,使用YYModel实现json/Model映射。 - UI布局方式:StoryBoard/Xib和代码+Masonry两种方式。
- 持久缓存:根据不同场景使用UserDefaults+NSCoding对象归档plist存储+Realm等。
- 设计模式:MVC模式。
文件目录概况
| ___ BrainProject
| | ___ AppDelegate.m(项目初始化)
| | ___ Request(网络模块相关)
| | | ___ CommonRequest(公用请求部分:封装了上传图片/视频等类方法)
| | | ___ BaseRequest(封装全局get/post请求方法,及域名统一管理)
| | | | __ KMRequestApi.h(全局域名管理,打包时需要注意请求的服务器地址)
| | | ___ Q_RAYRequest(Q_RAY部分网络请求部分:...)
| | | ___ ShoesPlanRequest(ShoesPlan部分网络请求部分:...)
| | | ___ TrainClassRequest(TrainClass部分网络请求部分:...)
| | | ___ Brain(Brain部分网络请求部分:...)
| | | ___ Shoes(Shoes部分网络请求部分:...)
| | | ___ Robot(Robot部分网络请求部分:...)
| | | ___ Login(Login部分网络请求部分:手机号登录/验证码登录/第三方登录/注册/找回密码/退出登录/...)
| | ___ Helpers
| | | ___ 待续...
| | ___ Protocol
| | | ___ 待续...
| | ___ Lib(使用的三方框架,单文件拖入项目中维护或者个别第三方库不支持pod导入及升级才手动导入)
| | | ___ OpenCV
| | | ___ SwiftyJSON
| | | ___ FDFullscreenPopGesture
| | | ___ MobPush
| | | ___ ShareSDK
| | ___ Extension(系统框架功能扩展)
| | ___ Class
| | ___ Sources
| | ___ Tools
| | ___ BaseModule
| | ___ Common
第三方库使用
项目使用 CocoaPods 管理使用三方框架。
建议: 网络,图片加载,缓存等基础组件库可以使用,跟 UI 相关尽量只做参考不要引入项目导致不便于维护与更新。
// 目前项目中使用到的第三方库
pod 'RealReachability'
pod 'MJRefresh'
pod 'SDWebImage'
pod 'YYCategories'
pod 'Masonry'
pod 'MBProgressHUD'
pod 'AFNetworking'
pod 'YYModel'
pod 'Bugly'
pod 'Realm'
pod 'YYText'
pod 'YYCache'
pod 'ZFPlayer', '~> 3.0'
pod 'ZFPlayer/ControlView', '~> 3.0'
pod 'ZFPlayer/AVPlayer', '~> 3.0'
pod 'TZImagePickerController'
Realm本地数据库使用说明:
- 安装realm;
使用cocoapods:
pod cache clean Realm
pod cache clean RealmSwift
pod deintegrate || rm -rf Pods
pod install --verbose
rm -rf ~/Library/Developer/Xcode/DerivedData
使用Carthage:
rm -rf Carthage
rm -rf ~/Library/Developer/Xcode/DerivedData
carthage update
- 构建继承自RLMObject的模型(注意设置主键及oc和swift的基本数据类型转换问题);
- 增删改查:
// 增
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
[realm addOrUpdateObject:thispage];
}];
// 删
[realm transactionWithBlock:^{
[realm deleteObject:thispage];
}];
// 改
QNPageModel *thisrepage = [[QNPageModel allObjects] firstObject];
[realm transactionWithBlock:^{
thisrepage.pagenum = 1000000;
}];
// 异步修改
// Query and update the result in another thread
dispatch_async(dispatch_queue_create("background", 0), ^{
@autoreleasepool {
QNPageModel *thisrepage2 = [[QNPageModel allObjects] firstObject];
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
thisrepage2.pagenum = 3;
[realm commitWriteTransaction];
}
});
// 查 单个对象
QNPageModel *thisrepage1 = [[QNPageModel allObjects] firstObject];
NSLog(@"%@", thisrepage1);
// 查 一组对象
RLMResults<QNNoteModel *> *allnotes = [QNNoteModel allObjects];
NSLog(@"%@", allnotes);
// 查 nid == 1的对象
QNPageModel *nid1model= [QNNoteModel objectsWhere:@"nid == 1"].firstObject;
NSLog(@"%@", nid1model);
- 模型属性变更涉及到的数据库变更
场景1: 旧版本的模型firstName和lastName是两个属性,新版本由于接口升级合并成fullName属性。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// The enumerateObjects:block: method iterates
// over every 'Person' object stored in the Realm file
[migration enumerateObjects:Person.className
block:^(RLMObject *oldObject, RLMObject *newObject) {
// combine name fields into a single field
newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
oldObject[@"firstName"],
oldObject[@"lastName"]];
}];
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
场景2:属性重命名(旧版本的模型age属性,新版本由于接口升级改成yearsSinceBirth属性)。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// The renaming operation should be done outside of calls to `enumerateObjects:`.
[migration renamePropertyForClass:Person.className oldName:@"yearsSinceBirth" newName:@"age"];
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
- 数据库线性迁移
- 实时同步
- 冲突解决
- 通知
// Observe Realm Notifications
token = [realm addNotificationBlock:^(NSString *notification, RLMRealm * realm) {
[myViewController updateUI];
}];
// 接收通知之后,局部刷新UI而不是重新加载所有内容
- (void)viewDidLoad {
[super viewDidLoad];
// Observe RLMResults Notifications
__weak typeof(self) weakSelf = self;
self.notificationToken = [[Person objectsWhere:@"age > 5"]
addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *changes, NSError *error) {
if (error) {
NSLog(@"Failed to open Realm on background worker: %@", error);
return;
}
UITableView *tableView = weakSelf.tableView;
// Initial run of the query will pass nil for the change information
if (!changes) {
[tableView reloadData];
return;
}
// Query results have changed, so apply them to the UITableView
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
}];
}
- (void)dealloc {
[self.notificationToken invalidate];
}
SDWebImage网络图片加载及缓存库 底层实现原理及内部实现过程:
- How To Use;
Objective-C:
#import <SDWebImage/SDWebImage.h>
...
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
Swift:
import SDWebImage
imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
- options选项:
//失败后重试
SDWebImageRetryFailed = 1 << 0,
//UI交互期间开始下载,导致延迟下载比如UIScrollView减速。
SDWebImageLowPriority = 1 << 1,
//只进行内存缓存
SDWebImageCacheMemoryOnly = 1 << 2,
//这个标志可以渐进式下载,显示的图像是逐步在下载
SDWebImageProgressiveDownload = 1 << 3,
//刷新缓存
SDWebImageRefreshCached = 1 << 4,
//后台下载
SDWebImageContinueInBackground = 1 << 5,
//NSMutableURLRequest.HTTPShouldHandleCookies = YES;
SDWebImageHandleCookies = 1 << 6,
//允许使用无效的SSL证书
//SDWebImageAllowInvalidSSLCertificates = 1 << 7,
//优先下载
SDWebImageHighPriority = 1 << 8,
//延迟占位符
SDWebImageDelayPlaceholder = 1 << 9,
//改变动画形象
SDWebImageTransformAnimatedImage = 1 << 10
- 实现流程:
1.【setImageWithURL:placeholderImage:options:】-> 显示placeholderImage,然后根据url开始处理图片;
2.【SDWebImageManager-downloadWithURL:delegate:options:userInfo:】-> SDImageCache从缓存查找图片是否已经下载,如果内存缓存中存在则直接回调到SDWebImageManager用于展示;
3.【NSInvocationOperation】-> 生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存;
4.【imageCache:didNotFindImageForKey:userInfo】-> 如果硬盘找不到,则回调notfind;
5.【SDWebImageDownloader】-> 由NSURLConnection来实现图片下载,connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果;
6.下载完成的图片在一个NSOperationQueue中做图片解码处理,不会拖慢主线程UI;
7.解码完成,回调显示;
8.【SDImageCache】图片内存缓存和硬盘缓存同时保存,在一个单独的NSInvocationOperation完成,避免拖慢主线程;
音视频合成及编解码实现过程:
- How To Use;
富文本模块(录音/插入图片视频):
- How To Use;
蓝牙模块:
- How To Use;
3d模块:
- How To Use;
OpenCV及CoreImage模块-矩形识别/裁剪/模糊匹配:
CoreImage实现矩形识别,实现步骤如下:
- CoreImage 下CIDetector.h自带了四种识别功能
/* 人脸识别 */
CORE_IMAGE_EXPORT NSString* const CIDetectorTypeFace NS_AVAILABLE(10_7, 5_0);
/* 矩形边缘识别 */
CORE_IMAGE_EXPORT NSString* const CIDetectorTypeRectangle NS_AVAILABLE(10_10, 8_0);
/* 二维码识别 */
CORE_IMAGE_EXPORT NSString* const CIDetectorTypeQRCode NS_AVAILABLE(10_10, 8_0);
/* 文本识别 */
#if __OBJC2__
CORE_IMAGE_EXPORT NSString* const CIDetectorTypeText NS_AVAILABLE(10_11, 9_0);
- 边缘检测:
[CIDetector detectorOfType:CIDetectorTypeRectangle context:nil options:@{CIDetectorAccuracy : CIDetectorAccuracyHigh}];
- 使用CAShapeLayer将边缘绘制并显示:
// 将图像空间的坐标系转换成uikit坐标系
TransformCIFeatureRect featureRect = [self transfromRealRectWithImageRect:imageRect topLeft:topLeft topRight:topRight bottomLeft:bottomLeft bottomRight:bottomRight];
// 边缘识别路径
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:featureRect.topLeft];
[path addLineToPoint:featureRect.topRight];
[path addLineToPoint:featureRect.bottomRight];
[path addLineToPoint:featureRect.bottomLeft];
[path closePath];
// 背景遮罩路径
UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:CGRectMake(-5,-5,self.frame.size.width + 10,self.frame.size.height + 10)];
[rectPath setUsesEvenOddFillRule:YES];
[rectPath appendPath:path];
_rectOverlay.path = rectPath.CGPath;
使用CGImage-CGImageCreateWithImageInRect实现图片裁剪,实现方式如下:
// imageRef
CGImageRef imageRef = image.CGImage;
// 传入原图片的imageRef以及rect获取一个新的CGImageRef
CGImageRef topimageref = CGImageCreateWithImageInRect(imageRef, toprect);
// 使用新的topimageref生成一个UIImageView
UIImageView *topImage = [[UIImageView alloc]initWithImage:[[UIImage alloc] initWithCGImage:topimageref]];
使用OpenCV实现图片匹配,实现方式如下:
- OpenCVManager工具类封装:
- 生成一张用矩形框标记目标的图片
- 将CMSampleBufferRef转为UIImage
- 将CMSampleBufferRef转为cv::Mat
- 局部自适应快速积分二值化方法
#pragma mark - 生成一张标记目标的图片
+ (UIImage *)imageWithColor:(UIColor *)rectColor size:(CGSize)size rectArray:(NSArray *)rectArray{
CGRect rect = CGRectMake(0, 0, size.width, size.height);
// 1.开启图片的图形上下文
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
// 2.获取
CGContextRef cxtRef = UIGraphicsGetCurrentContext();
// 3.矩形框标记颜色
//获取目标位置
for (NSInteger i = 0; i < rectArray.count; i++) {
NSValue *rectValue = rectArray[i];
CGRect targetRect = rectValue.CGRectValue;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:targetRect cornerRadius:5];
//加路径添加到上下文
CGContextAddPath(cxtRef, path.CGPath);
[rectColor setStroke];
[[UIColor clearColor] setFill];
//渲染上下文里面的路径
/**
kCGPathFill, 填充
kCGPathStroke, 边线
kCGPathFillStroke, 填充&边线
*/
CGContextDrawPath(cxtRef,kCGPathFillStroke);
}
//填充透明色
CGContextSetFillColorWithColor(cxtRef, [UIColor clearColor].CGColor);
CGContextFillRect(cxtRef, rect);
// 4.获取图片
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
// 5.关闭图形上下文
UIGraphicsEndImageContext();
// 6.返回图片
return img;
}
#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;
}
#pragma mark - 将CMSampleBufferRef转为UIImage
+ (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer
{
// Get a CMSampleBuffer's Core Video image buffer for the media data
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// Get the number of bytes per row for the pixel buffer
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
// Get the number of bytes per row for the pixel buffer
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// Get the pixel buffer width and height
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// Create a device-dependent RGB color space
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// Create a bitmap graphics context with the sample buffer data
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
//先获取imgBuffer
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];
CIContext *temporaryContext = [CIContext contextWithOptions:nil];
CGImageRef videoImage = [temporaryContext
createCGImage:ciImage
fromRect:CGRectMake(0, 0,
CVPixelBufferGetWidth(imageBuffer),
CVPixelBufferGetHeight(imageBuffer))];
//再旋转90度
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, 0, height);
transform = CGAffineTransformRotate(transform, -M_PI_2);
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0,0,height,width), videoImage);
CGImageRelease(videoImage);
// Create a Quartz image from the pixel data in the bitmap graphics context
CGImageRef quartzImage = CGBitmapContextCreateImage(context);
// Unlock the pixel buffer
CVPixelBufferUnlockBaseAddress(imageBuffer,0);
// Free up the context and color space
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// Create an image object from the Quartz image
UIImage *image = [UIImage imageWithCGImage:quartzImage];
// Release the Quartz image
CGImageRelease(quartzImage);
return (image);
}
// 局部自适应快速积分二值化方法 https://blog.csdn.net/realizetheworld/article/details/46971143
+(cv::Mat)convToBinary:(cv::Mat) src{
cv::Mat dst;
cvtColor(src,dst,CV_BGR2GRAY);
int x1, y1, x2, y2;
int count=0;
long long sum=0;
int S=src.rows>>3; //划分区域的大小S*S
int T=15; /*百分比,用来最后与阈值的比较。原文:If the value of the current pixel is t percent less than this average
then it is set to black, otherwise it is set to white.*/
int W=dst.cols;
int H=dst.rows;
long long **Argv;
Argv=new long long*[dst.rows];
for(int ii=0;ii<dst.rows;ii++)
{
Argv[ii]=new long long[dst.cols];
}
for(int i=0;i<W;i++)
{
sum=0;
for(int j=0;j<H;j++)
{
sum+=dst.at<uchar>(j,i);
if(i==0)
Argv[j][i]=sum;
else
Argv[j][i]=Argv[j][i-1]+sum;
}
}
for(int i=0;i<W;i++)
{
for(int j=0;j<H;j++)
{
x1=i-S/2;
x2=i+S/2;
y1=j-S/2;
y2=j+S/2;
if(x1<0)
x1=0;
if(x2>=W)
x2=W-1;
if(y1<0)
y1=0;
if(y2>=H)
y2=H-1;
count=(x2-x1)*(y2-y1);
sum=Argv[y2][x2]-Argv[y1][x2]-Argv[y2][x1]+Argv[y1][x1];
if((long long)(dst.at<uchar>(j,i)*count)<(long long)sum*(100-T)/100)
dst.at<uchar>(j,i)=0;
else
dst.at<uchar>(j,i)=255;
}
}
for (int i = 0 ; i < dst.rows; ++i)
{
delete [] Argv[i];
}
delete [] Argv;
return dst;
}
- 具体实现步骤:
- 原图片转化为灰度矩阵;
- 获取视频帧,处理成灰度矩阵;
- 图像金字塔分级放大缩小匹配,最大0.8相机图像,最小0.3tep图像;
- 对比两个图像是否有相同区域;
- 保存当前模版矩阵匹配的位置cv::Point;
//将图片转换为灰度的矩阵
-(cv::Mat)initTemplateImage:(NSString *)imgName{
UIImage *templateImage = [UIImage imageNamed:imgName];
cv::Mat tempMat;
UIImageToMat(templateImage, tempMat);
cv::cvtColor(tempMat, tempMat, CV_BGR2GRAY);
return tempMat;
}
#pragma mark - 获取视频帧,处理视频
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
[NSThread sleepForTimeInterval:0.5];
cv::Mat imgMat;
imgMat = [OpenCVManager bufferToMat:sampleBuffer];
//判断是否为空,否则返回
if (imgMat.empty() || self.templateMat.empty()) {
return;
}
//转换为灰度图像
cv::cvtColor(imgMat, imgMat, CV_BGR2GRAY);
UIImage *tempImg = MatToUIImage(imgMat);
//获取标记的矩形
NSArray *rectArr = [self compareByLevel:6 CameraInput:imgMat];
//转换为图片
UIImage *rectImg = [OpenCVManager imageWithColor:[UIColor redColor] size:tempImg.size rectArray:rectArr];
CGImageRef cgImage = rectImg.CGImage;
//在异步线程中,将任务同步添加至主线程,不会造成死锁
dispatch_sync(dispatch_get_main_queue(), ^{
if (cgImage) {
self.tagLayer.contents = (__bridge id _Nullable)cgImage;
}
});
}
//图像金字塔分级放大缩小匹配,最大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<mid) {
//如果是前半个循环,先缩小处理
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;
}
/**
对比两个图像是否有相同区域
@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;
}
}
Vision+CoreML模块:
CoreMedia+Vision+CoreML实现人体肢体关节检测,实现步骤如下:
- Core ML模型导入到项目中(Core ML模型的产生通过其他机器学习工具训练然后转换成Core ML格式);
- 选择该文件,Xcode会生成了模型类的输入输出类及一个主类,主类包括model属性和两个prediction方法;
- Vision 框架会负责把我们熟悉的图片格式转换成GoogLeNetPlacesInput类中使用到的CVPixelBuffer 类型的 sceneImage。
- Vision 框架还会把 GoogLeNetPlacesOutput 属性转换为自己的 results 类型,并管理对 prediction方法的调用,所以在所有生成的代码中,我们只会使用 model 属性。
- 加载模型:
// 从生成的类中加载 ML 模型
guard let model = try? VNCoreMLModel(for: GoogLeNetPlaces().model) else {
fatalError("can't load Places ML model")
}
- 创建请求:
// 创建一个带有 completion handler 的 Vision 请求
let request = VNCoreMLRequest(model: model) { [weak self] request, error in
guard let results = request.results as? [VNClassificationObservation],
let topResult = results.first else {
fatalError("unexpected result type from VNCoreMLRequest")
}
// 在主线程上更新 UI
let article = (self?.vowels.contains(topResult.identifier.first!))! ? "an" : "a"
DispatchQueue.main.async { [weak self] in
self?.answerLabel.text = "\(Int(topResult.confidence * 100))% it's \(article) \(topResult.identifier)"
}
}
或者将请求写在visionModel的set方法里,如下:
var visionModel: VNCoreMLModel! {
didSet {
request = VNCoreMLRequest(model: visionModel, completionHandler: visionRequestDidComplete)
request.imageCropAndScaleOption = .scaleFill
}
}
- VNCoreMLRequest.results:
说明:当CoreML模型是分类器,而不是预测器或图像处理器时,Vision框架返回的是VNClassificationObservation对象数组。
VideoCapture.swift:摄像头开启及前后切换;
JointViewController.swift:加载model_cpm,请求分类;
网络模块设计
使用了 AFNetworking
处理所有的网络请求, 使用 YYModel
做 JSON的解析和映射。
编程风格
推荐使用 AOP 与 函数式 进行编程,使用 Extension
对原有功能进行扩展。
打包上传appstore
- 待续...
网友评论