今天看啥  ›  专栏  ›  zhuzilin

用 Swift 的你,想不想也来试试 ncnn?

zhuzilin  · 掘金  ·  · 2021-02-05 22:37
阅读 136

用 Swift 的你,想不想也来试试 ncnn?

2021 年了,深度学习的狂风也连续吹了四五年了~ 从人脸支付到直播美颜,不少的成果早已落地到我们日常的 app 中。对于移动端开发者来说,想把深度学习模型部署到自己的应用中,需要借助推理框架的力量。在众多的移动端推理框架中,ncnn 无疑是其中的佼佼者。

给不了解的朋友解释一下,ncnn 是 nihui 大大写的移动端深度学习推理框架,速度快,结构清晰,在 github 上已经有过万 star。如果你的应用中需要用到人工智能,那 ncnn 很有可能会帮上你的大忙。

不过有些遗憾的是,目前项目里的大多数例子都是安卓的,iOS 的寥寥无几,仅有的几个也都是基于 Objective-C 的,这不禁让用 Swift 写 iOS app 的我欲哭无泪...

既然没有,果断自己造轮子!看看如何在 Swift 中使用 ncnn。

因为 nihui 大大已经为我们提供了现成的 ncnn.framework,所以问题就简化为了如何在 Swift 中调用 C++ 库。

对 Swift 和 Objective-C 都有所了解的朋友可能知道,这两门语言有这样的特性:

  • Swift 不支持调用 C++,但是支持调用 Objective-C 的头文件;
  • Objective-C 和 Objective-C++ 用的其实是相同的头文件;
  • Objective-C++ 是可以调用 C++ 的。

所以实现的路径变成了:

1. 创建 Objective-C 文件以及 bridging header

在项目中新建一个 Objective-C 文件,命名为 NcnnWrapper.m,然后 Xcode 会自动为你生成该 Objective-C 文件的对应的头文件 NcnnWrapper.h,以及询问你是否也要创建一个 bridging header。 这个 bridging header (名字应该叫 Xxx-Bridging-Header.h)将会把对应的 Objective-C 代码自动转化为 Swift 可以调用的样子。

想要在 Swift 中使用 Objective-C 的函数或类,只需要在 bridging header 中加上我们创建的头文件:

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "NcnnWrapper.h"
复制代码

2. 把 Objective-C 文件重命名为 Objective-C++ 文件

下一步就是把 NcnnWrapper.m 文件重命名为 NcnnWrapper.mm ,这样 Xcode 就能把它识别为 Objective-C++ 文件了。在 Objective-C++ 文件中,我们就可以随意地调用 ncnn 的 C++ 代码。

3. 创建辅助用的中间结构体

虽然我们的 .mm 文件里面可以写 C++ 了,但是在头文件中还是不能出现 C++ 特有的语法。这就需要一个过渡用的结构体。用代码来说,大致是:

// NcnnWrapper.h
struct _Net;
@interface NcnnNet : NSObject
{
    @public struct _Net *_net;
}
- (int)loadParam:(NSString*)paramPath;
...
@end

// NcnnWrapper.mm
// MARK: Net
struct _Net {
    ncnn::Net _net;
};

@implementation NcnnNet
...
- (int)loadParam:(NSString *)paramPath
{
    return _net->_net.load_param([paramPath UTF8String]);
}
...
@end
复制代码

在上面的代码中,通过 _Net 结构体,我们顺利的把 ncnn::Net 留在了 NcnnWrapper.mm 文件中。之后,我们就可以在 Swift 里面直接使用 NcnnNet 类了。

根据上面这种方式,我在 ncnn-swift 中写了 2 个例子:

图片分类—— SqueezenetSwift

SwiftUI 让应用自动适应了深色模式

这个项目就是从项目中读取猫猫的图片,然后用 squeezenet 做一下分类,项目的代码主要是参照的 nihui/ncnn-android-squeezenet。模型部分的 Swift 代码如下:

class Squeezenet {
    ...
    let net: NcnnNet
    
    init?() {
        // 初始化网络,并加载相关参数
        net = NcnnNet()
        let paramBinPath = Bundle.main.path(forResource: "squeezenet_v1.1.param", ofType: "bin")
        guard net.loadParamBin(paramBinPath) == 0 else {
            return nil
        }
        let modelPath = Bundle.main.path(forResource: "squeezenet_v1.1", ofType: "bin")
        guard net.loadModel(modelPath) == 0 else {
            return nil
        }
        ...
    }
    
    func predict(for image: UIImage, top: Int = 1) -> [(Int, Float)] {
        ...
        let inputData: Data = ...
        // 创建输入
        //     65540 is ncnn::Mat::PIXEL_RGBA2RGB
        let input: NcnnMat = NcnnMat.init(fromPixels: inputData, 65540, 227, 227)
        let mean: [NSNumber] = [NSNumber(value: 104.0), NSNumber(value: 117.0), NSNumber(value: 123.0)]
        // 对输入进行归一化
        input.substractMeanNormalize(mean, nil)
        // 运行网络,并获得输出
        //     BLOB_data is 0, BLOB_prob is 82
        let output: [NSNumber: NcnnMat] = net.run([0: input], [82])
        // 将输出转化回 Swift 的数组
        let outputData: Data = output[82]!.toData()!
        let outputProb: [Float] = outputData.toArray(type: Float.self)
        ...
    }
}
复制代码

和安卓版的代码比对,会发现 Swift 代码和 C++ 代码的对应关系的还是比较清楚的。

物体检测—— YoloV5Swift

这个目标检测的项目也是参考的 nihui 大大的 ncnn-android-yolov5。这个项目里面使用的 wrapper 比上一个要更完善一些。除去支持了更多的 wrapper 的接口,这个项目主要是想展示一下该如何去创建 custom layer。在 ncnn 中,注册 custom layer 是先利用 DEFINE_LAYER_CREATORDEFINE_LAYER_DESTROYER 两个宏,注册层的创建和销毁函数,例如:

DEFINE_LAYER_CREATOR(YoloV5Focus)
复制代码

上面这条宏相当于实现了 YoloV5Focus_layer_creator 函数。之后用

net.register_custom_layer("YoloV5Focus", YoloV5Focus_layer_creator);
复制代码

这样的接口来进行注册。

从实现 Swift wrapper 的角度,我希望用户能够继续复用之前 C++ 的 custom layer 的实现,但是又不希望用户侵入 wrapper 的 C++ 代码中进行注册。所以就参照 tensorflow 中注册 custom op 的设计模式,采用了这样的一个宏:

#define DEFINE_CUSTOM_LAYER(name, T)                            \
    CustomLayerRegistrar<T> name##CustomLayerRegistrar(#name)
复制代码

还是拿上面的 YoloV5Focus 来举例,在定义完 custom layer 的类后,在对应的 .cpp 文件中加上:

DEFINE_CUSTOM_LAYER(YoloV5Focus, YoloV5Focus);
复制代码

这样就会创建一个 CustomLayerRegistrar<YoloV5Focus> 变量,而这个类的构造函数为:

    CustomLayerRegistrar(const std::string& name) {
        CustomLayerRegistry::Entry entry;
        entry.creator = [](void *) -> ncnn::Layer* {
            return new T;
        };
        CustomLayerRegistry::Global()->Register(name, entry);
    }
复制代码

也就是会自动把层保存在 CustomLayerRegistry::Global() 中。

然后在 Swift 代码中,只需要 net.registerCustomLayer("YoloV5Focus"),网络就会从 CustomLayerRegistry::Global() 中获取这个名字对应的实现,从而成功注册。

在性能上来说,上图中的时间还包括了约 40ms 的 opencv imread 的时间,输入图片基本保持原长宽比例,被缩放至长边为 640。这个性能基本可以和原项目中的 讨论 对齐。


最后,为了让更多 iOS 开发者,尤其是独立开发者们享受到 ncnn 的强大功能,我希望能够基于上面的一些设计为 ncnn 做一个 cocoapod,类似 tensorflow lite 推出的 TensorFlowLiteSwift。对这项工作有兴趣的朋友可以联系我,或者在下面的 issue 中留言讨论(感谢 nihui 大大在 issue 中的支持):

support Swift for ncnn · Issue #2642 · Tencent/ncnn

也希望感兴趣的朋友给 ncnn-swift 点个星呀~




原文地址:访问原文地址
快照地址: 访问文章快照