深度学习已经在生活的方方面面被应用和重视。随着手机算力的不断提升,以及深度学习的快速发展,特别是小网络模型不断成熟,原本在云端执行的推理预测就可以转移到端上来做。端智能即在端侧部署运行 AI 算法,相比服务端智能,端智能具有低延时、兼顾数据隐私、节省云端资源等优势。目前端智能正逐渐变为趋势,从业界来看,它已经在 AI 摄像、视觉特效等场景发挥了巨大价值。
目前如何将神经网络部署到端侧,一般的模式是“云侧训练,端侧推理”。也就是说,把用户的数据传到云侧上面,云侧基于获得的多数据进行训练模型,训练好的模型直接下放到端侧,然后端侧利用训练好的模型直接进行推理。由于云端数据中心有更高的处理能力和更宽松的能耗限制,所以这种做法是更容易实践的。但这样的做法存在一个比较重要的问题:隐私问题。端设备收集的使用数据常常带有用户隐私:例如邮件App 得到的用户回复文本都可能含有用户私人信息。近几年的一系列事件唤起了用户对隐私的慎重态度:欧盟发布了GDPR 法案,限制科技公司从用户收集数据。除此之外,对于高访问量的应用需要调用大量的服务端部署资源来部署识别模型;另一方面,DL 在云端则意味着数据必须上传。即使不考虑计算压力,从网络延时、流量、隐私保护等角度,也给用户体验带来种种限制。因此,对相当多的应用来说,DL 模型前移到移动端部署可以看作是一种刚需。
一般来说,端侧深度学习的应用可以分成如下几个阶段:
训练框架训练模型->模型转化为ONNX格式->ONNX格式转化为其他格式(NCNN,TNN,MNN的模型格式)->在对应的推理框架中部署
对于不同的推理框架,虽然细节部分略有不同,但是整体的流程则都是上面这样。
ONNX:(Open Neural Network Exchange):开放神经网络交换是一种针对机器学习算法所设计的开放式文件格式标准,用于存储训练好的算法模型。许多主流的深度学习框架(如 PyTorch、TensorFlow、MXNet)都支持将模型导出为 ONNX 模型。ONNX 使得不同的深度学习框架可以以一种统一的格式存储模型数据以及进行交互。
ONNX Runtime:ONNX运行时,支持Linux、Windows、Mac OS、Android、iOS等多种平台和多种硬件(CPU、GPU、NPU等)上进行模型部署和推理。
关于不同框架的分析可以参考如下文章:
深度学习框架大PK:TNN决战MNN,ncnn依旧经典
NCNN
ncnn 是一个为手机端极致优化的高性能神经网络前向计算框架。 ncnn 从设计之初深刻考虑手机端的部署和使用。 无第三方依赖,跨平台,手机端 cpu 的速度快于目前所有已知的开源框架。 基于 ncnn,开发者能够将深度学习算法轻松移植到手机端高效执行, 开发出人工智能 APP,将 AI 带到你的指尖。 ncnn目前已在腾讯多款应用中使用,如:QQ,Qzone,微信,天天P图等。
NCNN作为最早开源的一批深度学习推理框架,拥有着较为完善的社区和问题解决,基本上在开发过程中遇到的大多数问题都能够找到相应的解决方法。这个链接是NCNN的官方处理issue的地方,遇到了部署的问题可以首先在这里搜一下。
• https://github.com/nihui/ncnn-android-squeezenet
• https://github.com/nihui/ncnn-android-styletransfer
• https://github.com/nihui/ncnn-android-mobilenetssd
• https://github.com/moli232777144/mtcnn_ncnn
• https://github.com/nihui/ncnn-android-yolov5
• https://github.com/xiang-wuu/ncnn-android-yolov7
• https://github.com/nihui/ncnn-android-scrfd
• https://github.com/shaoshengsong/qt_android_ncnn_lib_encrypt_example
调用一般流程,涉及到数据的输入输出:
#include
#include
#include "net.h"
int main()
{
//外部传入图片
cv::Mat img = cv::imread("image.ppm", CV_LOAD_IMAGE_GRAYSCALE);
int w = img.cols;
int h = img.rows;
// subtract 128, norm to -1 ~ 1
//重新resize大小到模型输入
ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_GRAY, w, h, 60, 60);
float mean[1] = { 128.f };
float norm[1] = { 1/128.f };
//图片归一化
in.substract_mean_normalize(mean, norm);
//定义ncnn网络
ncnn::Net net;
//加载模型结构
net.load_param("model.param");
//加载模型参数
net.load_model("model.bin");
//创建模型推理器
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.set_num_threads(4);
//数据输入推理器
ex.input("data", in);
//创建输出矩阵
ncnn::Mat feat;
//模型推理
ex.extract("output", feat);
return 0;
}
我们以YOLOV5的demo为例,来总结如何快速部署测试自己的模型。需要注意的是,YOLOv5是ncnn已经有框架和实现的,所以大多数的部署教程都是针对fine-tunning的方式来部署,例如下面这一篇。我们先分析这个。关于自己创建模型,后面再分析。
具体的训练和Android Studio的安装和环境搭建,可以参考下面这篇文章YOLOV5从训练到部署测试NCNN安卓端部署保姆级教程,这篇文章的步骤可以跑一下,安装Cmake和Sdk。当然如果从来都没有接触过这一块的话,即使把demo跑通了,应该也还是很蒙的,这里我把最重要的一个文件单摘出来,添加相关的注释帮助理解:
ncnn-android-yolov5/app/src/main/jni/yolov5ncnn_jni.cpp
选取和模型相关的函数分析
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
__android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "JNI_OnLoad");
ncnn::create_gpu_instance();
return JNI_VERSION_1_4;
}
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
{
__android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "JNI_OnUnload");
ncnn::destroy_gpu_instance();
}
// public native boolean Init(AssetManager mgr);
JNIEXPORT jboolean JNICALL Java_com_tencent_yolov5ncnn_YoloV5Ncnn_Init(JNIEnv* env, jobject thiz, jobject assetManager)
{
ncnn::Option opt;
opt.lightmode = true;
opt.num_threads = 4;
opt.blob_allocator = &g_blob_pool_allocator;
opt.workspace_allocator = &g_workspace_pool_allocator;
opt.use_packing_layout = true;
// use vulkan compute
if (ncnn::get_gpu_count() != 0)
opt.use_vulkan_compute = true;
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
yolov5.opt = opt;
yolov5.register_custom_layer("YoloV5Focus", YoloV5Focus_layer_creator);
// init param
// 模型构建与导入源码平平无奇,就是detect_yolov5方法中的以下内容
// ncnn的模型构建与权重导入其实做的工作就是:创建空白Net对象,导入模型结构文件(param)与权重文件(bin)
// 可能还有一些其他操作,比如自定义Layer,即register_custom_layer操作
{
int ret = yolov5.load_param(mgr, "yolov5s.param");
if (ret != 0)
{
__android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "load_param failed");
return JNI_FALSE;
}
}
// init bin
{
int ret = yolov5.load_model(mgr, "yolov5s.bin");
if (ret != 0)
{
__android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "load_model failed");
return JNI_FALSE;
}
}
// init jni glue
// 初始化java需要的一些参数
jclass localObjCls = env->FindClass("com/tencent/yolov5ncnn/YoloV5Ncnn$Obj");
objCls = reinterpret_cast<jclass>(env->NewGlobalRef(localObjCls));
constructortorId = env->GetMethodID(objCls, "" , "(Lcom/tencent/yolov5ncnn/YoloV5Ncnn;)V");
xId = env->GetFieldID(objCls, "x", "F");
yId = env->GetFieldID(objCls, "y", "F");
wId = env->GetFieldID(objCls, "w", "F");
hId = env->GetFieldID(objCls, "h", "F");
labelId = env->GetFieldID(objCls, "label", "Ljava/lang/String;");
probId = env->GetFieldID(objCls, "prob", "F");
return JNI_TRUE;
}
// public native Obj[] Detect(Bitmap bitmap, boolean use_gpu);
JNIEXPORT jobjectArray JNICALL Java_com_tencent_yolov5ncnn_YoloV5Ncnn_Detect(JNIEnv* env, jobject thiz, jobject bitmap, jboolean use_gpu)
{
if (use_gpu == JNI_TRUE && ncnn::get_gpu_count() == 0)
{
return NULL;
//return env->NewStringUTF("no vulkan capable gpu");
}
// 模型推理包括几个部分:
// 构建模型输入,并与模型类(即Net)绑定。
// 模型输入构建,主要工作包括
// resize图像,令长边为target_size,短边按比例变化
// 执行pad操作,令边长能够被32整除
// 定义norm相关
// 将模型输入与模型绑定
double start_time = ncnn::get_current_time();
// 获取当前图片的尺寸
AndroidBitmapInfo info;
AndroidBitmap_getInfo(env, bitmap, &info);
const int width = info.width;
const int height = info.height;
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
return NULL;
// ncnn from bitmap
// 长边为 target_size,短边按比例变化
const int target_size = 640;
// letterbox pad to multiple of 32
int w = width;
int h = height;
float scale = 1.f;
if (w > h)
{
scale = (float)target_size / w;
w = target_size;
h = h * scale;
}
else
{
scale = (float)target_size / h;
h = target_size;
w = w * scale;
}
// resize 图片
ncnn::Mat in = ncnn::Mat::from_android_bitmap_resize(env, bitmap, ncnn::Mat::PIXEL_RGB, w, h);
// 执行pad操作,令图片尺寸可以被32整除
// pad to target_size rectangle
// yolov5/utils/datasets.py letterbox
int wpad = (w + 31) / 32 * 32 - w;
int hpad = (h + 31) / 32 * 32 - h;
ncnn::Mat in_pad;
// 实现pad操作
ncnn::copy_make_border(in, in_pad, hpad / 2, hpad - hpad / 2, wpad / 2, wpad - wpad / 2, ncnn::BORDER_CONSTANT, 114.f);
// yolov5
std::vector<Object> objects;
{
const float prob_threshold = 0.25f;
const float nms_threshold = 0.45f;
// norm 操作参数
const float norm_vals[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};
in_pad.substract_mean_normalize(0, norm_vals);
ncnn::Extractor ex = yolov5.create_extractor();
ex.set_vulkan_compute(use_gpu);
ex.input("images", in_pad);
std::vector<Object> proposals;
// anchor setting from yolov5/models/yolov5s.yaml
// 执行模型推理,主要功能包括
// 根据模型结果、anchors等参数构建bbox
// 实现NMS
// 将bbox尺寸转换为图像原始尺寸
// yolov5 的输出由3个特征图生成,即下面的 stride 8/16/32
// 每个部分都是根据 anchors 和输出结果构建最终预测结果(即bbox)
// 预测结果都保存到 proposals 中,通过 Object 对象保存
// stride 8
{
ncnn::Mat out;
ex.extract("output", out);
ncnn::Mat anchors(6);
anchors[0] = 10.f;
anchors[1] = 13.f;
anchors[2] = 16.f;
anchors[3] = 30.f;
anchors[4] = 33.f;
anchors[5] = 23.f;
std::vector<Object> objects8;
generate_proposals(anchors, 8, in_pad, out, prob_threshold, objects8);
proposals.insert(proposals.end(), objects8.begin(), objects8.end());
}
// stride 16
{
ncnn::Mat out;
ex.extract("781", out);
ncnn::Mat anchors(6);
anchors[0] = 30.f;
anchors[1] = 61.f;
anchors[2] = 62.f;
anchors[3] = 45.f;
anchors[4] = 59.f;
anchors[5] = 119.f;
std::vector<Object> objects16;
generate_proposals(anchors, 16, in_pad, out, prob_threshold, objects16);
proposals.insert(proposals.end(), objects16.begin(), objects16.end());
}
// stride 32
{
ncnn::Mat out;
ex.extract("801", out);
ncnn::Mat anchors(6);
anchors[0] = 116.f;
anchors[1] = 90.f;
anchors[2] = 156.f;
anchors[3] = 198.f;
anchors[4] = 373.f;
anchors[5] = 326.f;
std::vector<Object> objects32;
generate_proposals(anchors, 32, in_pad, out, prob_threshold, objects32);
proposals.insert(proposals.end(), objects32.begin(), objects32.end());
}
// 根据score对所有proposals排序
// sort all proposals by score from highest to lowest
qsort_descent_inplace(proposals);
// apply nms with nms_threshold
std::vector<int> picked;
nms_sorted_bboxes(proposals, picked, nms_threshold);
int count = picked.size();
// resize 所有bbox到原始尺寸
objects.resize(count);
for (int i = 0; i < count; i++)
{
objects[i] = proposals[picked[i]];
// adjust offset to original unpadded
float x0 = (objects[i].x - (wpad / 2)) / scale;
float y0 = (objects[i].y - (hpad / 2)) / scale;
float x1 = (objects[i].x + objects[i].w - (wpad / 2)) / scale;
float y1 = (objects[i].y + objects[i].h - (hpad / 2)) / scale;
// clip
x0 = std::max(std::min(x0, (float)(width - 1)), 0.f);
y0 = std::max(std::min(y0, (float)(height - 1)), 0.f);
x1 = std::max(std::min(x1, (float)(width - 1)), 0.f);
y1 = std::max(std::min(y1, (float)(height - 1)), 0.f);
objects[i].x = x0;
objects[i].y = y0;
objects[i].w = x1 - x0;
objects[i].h = y1 - y0;
}
}
// objects to Obj[]
static const char* class_names[] = {
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
"skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",
"tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
"potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone",
"microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear",
"hair drier", "toothbrush"
};
jobjectArray jObjArray = env->NewObjectArray(objects.size(), objCls, NULL);
for (size_t i=0; i<objects.size(); i++)
{
jobject jObj = env->NewObject(objCls, constructortorId, thiz);
env->SetFloatField(jObj, xId, objects[i].x);
env->SetFloatField(jObj, yId, objects[i].y);
env->SetFloatField(jObj, wId, objects[i].w);
env->SetFloatField(jObj, hId, objects[i].h);
env->SetObjectField(jObj, labelId, env->NewStringUTF(class_names[objects[i].label]));
env->SetFloatField(jObj, probId, objects[i].prob);
env->SetObjectArrayElement(jObjArray, i, jObj);
}
double elasped = ncnn::get_current_time() - start_time;
__android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "%.2fms detect", elasped);
return jObjArray;
}
从上面NCNN调用YOLOv5的例子,我们发现,实际上我们还是用了NCNN提供的模型和网络结构,具体的文件在这里:
ncnn-android-yolov5/app/src/main/assets/
接下来我们将从0到1部署一次模型
Pytorch保存模型有两种方法:
第一种方法只保存模型参数。第二种方法保存完整模型。推荐使用第一种,第二种方法可能在切换设备和目录的时候出现各种问题。
print(model.state_dict().keys()) # 输出模型参数名称
# 保存模型参数到路径"./data/model_parameter.pkl"
torch.save(model.state_dict(), "./data/model_parameter.pkl")
new_model = Model() # 调用模型Model
new_model.load_state_dict(torch.load("./data/model_parameter.pkl")) # 加载模型参数
new_model.forward(input) # 进行使用
torch.save(model, './data/model.pkl') # 保存整个模型
new_model = torch.load('./data/model.pkl') # 加载模型
目前大多数应用都会仅仅保存模型参数,用的时候创建模型结构,然后加载模型。
以pytorch为例,保存的模型为.pkl或者.model格式,可以用Netron打开查看。
接下来,是把模型参数文件和结构打包成ONNX的格式:
# onnx模型导入
import onnx
import torch
import torch.nn as nn
import torchvision.models as models
class Net(nn.Module):
def __init__(self, prior_size):
…………………
def forward(self, x):
…………………
return x
model = Net(prior_size)
model.load_state_dict(torch.load('xxx.model', map_location='cpu'))
model.eval()
input_names = ['input']
output_names = ['output']
# 规定输入尺寸
x = torch.randn(1, 3, 240, 320, requires_grad=True)
torch.onnx.export(model, x, 'best.onnx', input_names=input_names, output_names=output_names, verbose='True')
验证转换是否成功:
# 验证onnx模型
import onnx
model_onnx = onnx.load("best.onnx") # 加载onnx模型
onnx.checker.check_model(model_onnx) # 验证onnx模型是否成功导出
# 如果没有报错,表示导出成功
#####################################################################################
# #测试onnx 模型
import onnxruntime
import numpy as np
# 创建会话
# session = onnxruntime.InferenceSession("best.onnx", providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
session = onnxruntime.InferenceSession("best.onnx", providers=['CPUExecutionProvider'])
x = np.random.randn(1, 3, 240, 320)
ort_input = {session.get_inputs()[0].name: x.astype(np.float32)}
ort_output = session.run(None, ort_input)
# # 如果没有报错,表示执行成功
经过上面的处理后,会得到一个名为best.onnx的文件。
省去编译转换工具的时间,开箱即用,一键转换
利用模型转换网站,可以很方便的转化各种模型格式,而且网站是在本地处理,不必担心泄露。
输入ONNX,输出NCNN
得到两个文件一个.bin文件,用来存储具体的参数,一个.param文件,用来存储模型的框架
首先把文件模型放到路径:
app/src/main/assets
如果想从零开始构建一个自己的模型加载使用cpp文件,可以参考上面的官方案例,上面详细的写了输入输出部分该调用ncnn的哪个接口。
我们下面依然以一个demo的例子来说明问题,是风格迁移的例子:
在文件app/src/main/jni/styletransferncnn_jni.cpp中,
我们主要需要修改以下几个部分:
初始化部分:load我们的参数和模型作为其中一个可调用模型
// public native boolean Init(AssetManager mgr);
JNIEXPORT jboolean JNICALL Java_com_tencent_styletransferncnn_StyleTransferNcnn_Init(JNIEnv* env, jobject thiz, jobject assetManager)
{
ncnn::Option opt;
opt.lightmode = true;
opt.num_threads = 4;
opt.blob_allocator = &g_blob_pool_allocator;
opt.workspace_allocator = &g_workspace_pool_allocator;
// use vulkan compute
if (ncnn::get_gpu_count() != 0)
opt.use_vulkan_compute = true;
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
const char* model_paths[5] = {"opt.bin", "mosaic.bin", "pointilism.bin", "rain_princess.bin", "udnie.bin"};
for (int i=0; i<5; i++)
{
double start_time = ncnn::get_current_time();
int ret0;
int ret1;
styletransfernet[i].opt = opt;
if (i == 0){
//我们自己的模型替代掉第一个模型
ret0 = styletransfernet[i].load_param(mgr, "optfp16.param");
ret1 = styletransfernet[i].load_model(mgr, "optfp16.bin");
}else{
ret0 = styletransfernet[i].load_param(styletransfer_param_bin);
ret1 = styletransfernet[i].load_model(mgr, model_paths[i]);
}
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "load %d %d", ret0, ret1);
double elasped = ncnn::get_current_time() - start_time;
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "%.2fms loadtime", elasped);
}
return JNI_TRUE;
}
调用部分:修改输入的格式,维度
输出部分:定义输出的格式
// public native Bitmap StyleTransfer(Bitmap bitmap, int style_type, boolean use_gpu);
JNIEXPORT jboolean JNICALL Java_com_tencent_styletransferncnn_StyleTransferNcnn_StyleTransfer(JNIEnv* env, jobject thiz, jobject bitmap, jint style_type, jboolean use_gpu)
{
if (style_type < 0 || style_type >= 5)
return JNI_FALSE;
if (use_gpu == JNI_TRUE && ncnn::get_gpu_count() == 0)
return JNI_FALSE;
double start_time = ncnn::get_current_time();
AndroidBitmapInfo info;
AndroidBitmap_getInfo(env, bitmap, &info);
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
return JNI_FALSE;
int width = info.width;
int height = info.height;
const int downscale_ratio = 1;
// 重新定义输入的维度,利用ncnn自带的插值
ncnn::Mat in = ncnn::Mat::from_android_bitmap_resize(env, bitmap, ncnn::Mat::PIXEL_BGR, 320, 240);
const float mean_vals[3] = { 0.f, 0.f, 0.f };
const float norm_vals[3] = { 1/255.f, 1/255.f, 1/255.f };
// 图像归一化,把每个像素归一化到0-1
in.substract_mean_normalize(mean_vals, norm_vals);
// 定义输出矩阵
ncnn::Mat out;
{
ncnn::Extractor ex = styletransfernet[style_type].create_extractor();
ex.set_vulkan_compute(use_gpu);
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "in %d %d %d", in.w, in.h, in.c);
// ex.input(styletransfer_param_id::BLOB_input1, in);
// 注意这里的名字用的是转换onnx时起的名字
ex.input("input", in);
const float* ptr = in.channel(0);
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "valueuse %f", ptr[0]);
// ex.extract(styletransfer_param_id::BLOB_output1, out);
//真正的调用是在这里发生的
// 注意这里的名字用的是转换onnx时起的名字
ex.extract("output", out);
const float mean_vals[3] = { 0.f, 0.f, 0.f };
const float norm_vals[3] = { 255.f, 255.f,255.f };
// 去归一化,让像素值回到0-255
out.substract_mean_normalize(mean_vals, norm_vals);
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "out %d %d", out.w, out.h);
const float* pt = in.channel(0);
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "valueout %f", pt[0]);
}
// ncnn to bitmap
out.to_android_bitmap(env, bitmap, ncnn::Mat::PIXEL_GRAY);
double elasped = ncnn::get_current_time() - start_time;
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "%.2fms styletransfer", elasped);
return JNI_TRUE;
}
经过上面的操作,原来的candy模型已经被替换成了我们的模型,可以直接使用测试了
也可以打log来测试耗时:
double start_time = ncnn::get_current_time();
double elasped = ncnn::get_current_time() - start_time;
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "%.2fms loadtime", elasped);
然后在android studio的log里面filter选择StyleTransferNcnn即可看到模型的耗时。
输出Mat的内容
ncnn没有提供可以直接输出Mat数据的函数,所以想要输出Mat数据时,只能利用for循环进行遍历
void pretty_print(const ncnn::Mat& m)
{
for (int q=0; q<m.c; q++)
{
const float* ptr = m.channel(q);
for (int y=0; y<m.h; y++)
{
for (int x=0; x<m.w; x++)
{
printf("%f ", ptr[x]);
}
ptr += m.w;
printf("\n");
}
printf("------------------------\n");
}
}
可视化Mat
void visualize(const char* title, const ncnn::Mat& m)
{
std::vector<cv::Mat> normed_feats(m.c);
for (int i=0; i<m.c; i++)
{
cv::Mat tmp(m.h, m.w, CV_32FC1, (void*)(const float*)m.channel(i));
cv::normalize(tmp, normed_feats[i], 0, 255, cv::NORM_MINMAX, CV_8U);
cv::cvtColor(normed_feats[i], normed_feats[i], cv::COLOR_GRAY2BGR);
// check NaN
for (int y=0; y<m.h; y++)
{
const float* tp = tmp.ptr<float>(y);
uchar* sp = normed_feats[i].ptr<uchar>(y);
for (int x=0; x<m.w; x++)
{
float v = tp[x];
if (v != v)
{
sp[0] = 0;
sp[1] = 0;
sp[2] = 255;
}
sp += 3;
}
}
}
int tw = m.w < 10 ? 32 : m.w < 20 ? 16 : m.w < 40 ? 8 : m.w < 80 ? 4 : m.w < 160 ? 2 : 1;
int th = (m.c - 1) / tw + 1;
cv::Mat show_map(m.h * th, m.w * tw, CV_8UC3);
show_map = cv::Scalar(127);
// tile
for (int i=0; i<m.c; i++)
{
int ty = i / tw;
int tx = i % tw;
normed_feats[i].copyTo(show_map(cv::Rect(tx * m.w, ty * m.h, m.w, m.h)));
}
cv::resize(show_map, show_map, cv::Size(0,0), 2, 2, cv::INTER_NEAREST);
cv::imshow(title, show_map);
}