轻量级高性能推理引擎MNN 学习笔记 03.在iOS运行MNN的示例

发布于:2025-05-16 ⋅ 阅读:(9) ⋅ 点赞:(0)

在iOS运行MNN的示例

详细请参考iOS示例 ,完整代码请在github下载

模型下载与转换:

首先编译(如果已编译可以跳过)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++ 接口

主要思路

  1. 确定选择后端类型(CPU、Metal,默认为CPU)及线程数量(默认值为4)
  2. 确定算法模型
  3. 创建Interpreter & Session
  4. 对图像进行预处理
  5. 执行推理
  6. 将推理结果进行显示(只能在主线程中执行

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(&copy);
    }
    
    // 计算总耗时
    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 界面只能在主线程中执行

以下代码中重点关注以下函数:

  1. dispatch_async :在指定的队列中异步执行任务
  2. dispatch_get_global_queue :获取全局队列
  3. 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;
            });
        });
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到