在iOS运行MNN的示例
模型下载与转换:
首先编译(如果已编译可以跳过)MNNConvert,操作如下:
cd MNN
mkdir build && cd build
cmake -DMNN_BUILD_CONVERTER=ON ..
make -j8
然后下载并转换模型: 切到编译了 MNNConvert 的目录,如上为 build 目录,执行
sh ../tools/script/get_model.sh
注意:此处需要开启vpn 进行下载模型。
编译运行:
代码位置:project/ios
使用xcode打开project/ios/MNN.xcodeproj, target选择demo,既可编译运行。
效果示例:
代码说明
iOS demo 工程用Objective-C编写,集成了 MNN 的 C++ 接口
主要思路
- 确定选择后端类型(CPU、Metal,默认为CPU)及线程数量(默认值为4)
- 确定算法模型
- 创建Interpreter & Session
- 对图像进行预处理
- 执行推理
- 将推理结果进行显示(只能在主线程中执行)
setType 设置MNN推理引擎的运行类型和线程数
@implementation Model
/**
* 设置MNN推理引擎的运行类型和线程数
* @param type 运行类型(CPU或Metal GPU)
* @param threads 线程数量,用于CPU模式下的并行计算
*/
- (void)setType:(MNNForwardType)type threads:(NSUInteger)threads {
// 加锁保护,防止多线程并发访问导致的问题
std::unique_lock<std::mutex> _l(_mutex);
NSLog(@"setType: %d, threads: %lu", type, (unsigned long)threads);
// 如果已存在会话,先释放旧会话
if (_session) {
_net->releaseSession(_session);
}
// 如果GPU缓存未初始化,创建新的GPU资源缓存
if (nullptr == _cache) {
_cache.reset(new GpuCache);
}
// 配置MNN推理引擎的运行参数
MNN::ScheduleConfig config;
config.type = type; // 设置运行类型(CPU/GPU)
config.numThread = (int)threads; // 设置CPU线程数
// 根据运行类型进行不同的会话创建
if (type == MNN_FORWARD_METAL) {
// Metal GPU模式下的特殊配置
MNN::BackendConfig bnConfig;
MNNMetalSharedContext context;
// 设置Metal上下文,复用已创建的设备和命令队列
context.device = _cache->_device;
context.queue = _cache->_queue;
bnConfig.sharedContext = &context;
config.backendConfig = &bnConfig;
_session = _net->createSession(config);
} else {
// CPU模式下的标准配置
_session = _net->createSession(config);
}
// 获取会话的输入输出张量
_input = _net->getSessionInput(_session, nullptr); // 获取输入张量
_output = _net->getSessionOutput(_session, nullptr); // 获取输出张量
_type = type; // 保存当前运行类型
}
benchmark 执行模型推理的基准测试
/**
* 执行模型推理的基准测试
* @param cycles 测试循环次数,用于计算平均推理时间
* @return 返回包含平均推理时间的字符串,如果初始化失败返回nil
*/
- (NSString *)benchmark:(NSInteger)cycles {
// 加锁保护,确保多线程安全
std::unique_lock<std::mutex> _l(_mutex);
// 检查网络和会话是否正确初始化
if (!_net || !_session) {
return nil;
}
// 获取模型的输出张量并创建副本
MNN::Tensor *output = _net->getSessionOutput(_session, nullptr);
MNN::Tensor copy(output);
// 获取模型的输入张量并创建缓存
auto input = _net->getSessionInput(_session, nullptr);
MNN::Tensor tensorCache(input);
// 将输入数据复制到主机内存
input->copyToHostTensor(&tensorCache);
// 记录开始时间
NSTimeInterval begin = NSDate.timeIntervalSinceReferenceDate;
// 执行多次推理,用于计算平均时间
for (int i = 0; i < cycles; i++) {
// 将缓存的输入数据复制到输入张量
input->copyFromHostTensor(&tensorCache);
// 运行一次推理会话
_net->runSession(_session);
// 将输出结果复制到输出张量副本
output->copyToHostTensor(©);
}
// 计算总耗时
NSTimeInterval cost = NSDate.timeIntervalSinceReferenceDate - begin;
// 格式化输出结果,计算平均每次推理的耗时(毫秒)
NSString *string = @"";
return [string stringByAppendingFormat:@"time elapse: %.3f ms", cost * 1000.f / cycles];
}
inferImage 对输入图像UIImage
进行推理
/**
* 对输入图像进行推理
* @param image 输入的UIImage图像
* @param cycles 推理循环次数(本实现中未使用)
* @return 返回推理结果字符串
*/
- (NSString *)inferImage:(UIImage *)image cycles:(NSInteger)cycles {
// 加锁保护,确保线程安全
std::unique_lock<std::mutex> _l(_mutex);
// 获取输入图像的宽高
int w = image.size.width;
int h = image.size.height;
// 分配RGBA图像缓冲区,每个像素4个通道
unsigned char *rgba = (unsigned char *)calloc(w * h * 4, sizeof(unsigned char));
// 将UIImage转换为RGBA格式
{
CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
CGContextRef contextRef = CGBitmapContextCreate(rgba, w, h, 8, w * 4, colorSpace,
kCGImageAlphaNoneSkipLast | kCGBitmapByteOrderDefault);
CGContextDrawImage(contextRef, CGRectMake(0, 0, w, h), image.CGImage);
CGContextRelease(contextRef);
}
// 设置图像预处理参数
const float means[3] = {103.94f, 116.78f, 123.68f}; // 均值
const float normals[3] = {0.017f, 0.017f, 0.017f}; // 归一化参数
// 创建图像预处理器,设置输入格式为RGBA,输出格式为BGR
auto pretreat = std::shared_ptr<MNN::CV::ImageProcess>(
MNN::CV::ImageProcess::create(MNN::CV::RGBA, MNN::CV::BGR, means, 3, normals, 3));
// 设置图像缩放矩阵,将图像缩放到模型需要的大小(224x224)
MNN::CV::Matrix matrix;
matrix.postScale((w - 1) / 223.0, (h - 1) / 223.0);
pretreat->setMatrix(matrix);
// 获取模型的输入张量
auto input = _net->getSessionInput(_session, nullptr);
// 执行图像预处理并将结果写入输入张量
pretreat->convert(rgba, w, h, 0, input);
// 释放临时分配的图像缓冲区
free(rgba);
// 调用父类的推理方法执行实际的模型推理
return [super inferNoLock:0];
}
iOS OC 语法相关
更新UI 界面只能在主线程中执行
以下代码中重点关注以下函数:
- dispatch_async :在指定的队列中异步执行任务
- dispatch_get_global_queue :获取全局队列
- dispatch_get_main_queue :获取主线程队列
- (IBAction)benchmark {
// 检查相机是否在运行,只有在非相机模式下才能执行基准测试
if (!_session.running) {
// 禁用所有UI控件,防止测试过程中的用户交互
self.cameraItem.enabled = NO; // 禁用相机切换按钮
self.runItem.enabled = NO; // 禁用运行按钮
self.benchmarkItem.enabled = NO; // 禁用基准测试按钮
self.modelItem.enabled = NO; // 禁用模型选择按钮
self.forwardItem.enabled = NO; // 禁用推理后端选择按钮
self.threadItem.enabled = NO; // 禁用线程数选择按钮
// 在全局后台队列中异步执行基准测试
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行100次推理并获取性能统计结果
NSString *str = [self->_currentModel benchmark:100];
// 在主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 显示测试结果
self.resultLabel.text = str;
// 重新启用所有UI控件
self.cameraItem.enabled = YES;
self.runItem.enabled = YES;
self.benchmarkItem.enabled = YES;
self.modelItem.enabled = YES;
self.forwardItem.enabled = YES;
self.threadItem.enabled = YES;
});
});
}
}