1.基于opencv的缺陷检测
1.基于opencv DNN模块的UNet道路裂纹检测
- 对输入图像进行预处理,转为模型需要的输入格式数据(根据模型文件.onnx文件可以知道模型对输入数据的要求。)
- dnn::readNetFromONNX 读取模型完成推理与预测
- 解析输出结果(.onnx文件可以知道模型输出内容 例如name:203,type:float32[1,2,320,480], 这里的2表示背景和裂纹两个通道的预测得分,(二分类)通过比较两个得分哪个大,就知道是背景还是裂隙)
- 通过轮廓发现函数提取UNet模型推理之后的裂纹并绘制轮廓。(需要将result转为CV_8U)
2.基于opencv 差分图像二值分析的刀片缺陷检测
- 获取中间的ROI部分,对ROI部分进行轮廓发现与分析,获得每个刀片的位置。
- 对每个刀片根据二位模板计算差分图像。
- 对差分图像进行轮廓发现与分析找到华和与缺陷。
2.基于CUDA和openmp的近似最近邻检索
分别实现了基于kmeans++和kmeansⅡ的乘积量化PQ和向量量化VQ
1.VQ (两层VQ)
- 聚类两次
- 依次计算与聚类中心的距离,该聚类中心所属的向量的距离。
2.PQ
- M*96维向量可以分为3组32维或6组16维向量。 M*3*32
- 对每组做聚类,聚K类,得到K*3。用聚类中心代替原始向量。
- 查询向量同样分为3段,每段计算与聚类中心的距离。得到3组M个距离。
- 将3组距离相加得到M个距离再排序,得到topK.
3.kmeans++和kmeansⅡ
kmeansⅡ
- 初始一个聚类中心,最终生成l*iteration_times个聚类中心(2k*5个)
- 计算原始向量集到当前聚类中心集距离总和
- 计算所有向量可以加入聚类中心集的概率:2k*向量到聚类中心集的距离(到所有聚类中心距离的最小值)/第二步的距离总和。
- 取2k个加入聚类中心集,迭代5次得到2k*5个聚类中心。
k-means++算法选择初始聚类中心的基本原则是:初始的聚类中心之间的相互距离要尽可能的远。它选择初始聚类中心的步骤是:
- 1)从输入的数据点集合中随机选择一个点作为第一个聚类中心c1 ;
- 2)对于数据集中的每一个点x,计算它与最近聚类中心(指已选择的聚类中心)的距离D(x),并根据概率选择新的聚类中心ci ;
- 3)重复过程(2)直到找到k个聚类中心。
关于第二步:
- 先得到每个数据点的权重(kmeansⅡ得到聚类中心的所属向量的个数)
- 对于每个点计算 概率: 当前点的权重*当前点到已选择的聚类中心集的距离 / 所有数据点到已选择的聚类中心集的距离和
- 根据概率选择最大的,依次选择K个。然后进行普通的kmeans得到最终的K个聚类中心
3.基于毕昇C++的异构算子迁移
将图像处理库ITK中的itkSimilarityIndexImageFilter算子迁移到华为毕晟服务器上并且实现异构并行计算。相较于原算子实现了数十 倍的性能提升。
第一步:
vec_mul(vc, va, vb);
vec_cmp<EQ>(va, v_a_0);
vec_select(dst_a, v_a_0, v_a_1);
vec_cmp<EQ>(vb, v_b_0);
vec_select(dst_b, v_b_0, v_b_1);
vec_cmp<EQ>(vc, v_a_0);
vec_select(dst_intersection, v_a_0, v_a_1);
……
//主机端
for(int i = 0; i <= M * N; i++ ){
if(get_A[i] != 0){
m_CountOfImage1[threadId]++;
}
if(get_B[i] != 0){
m_CountOfImage1[threadId]++;
}
if(get_intersection[i] != 0){
m_CountOfImage1[threadId]++;
}
}
问题:vec_select,vec_cmpv有数据上限限制,结果要放到128位寄存器里,主机端相加慢
第二步:利用vec_cross_add向量求和。
vec_cmp<EQ>(va, v_0);
vec_select(dst_a, v_0, v_1);
vec_cmp<EQ>(vb, v_0);
vec_select(dst_b, v_0, v_1);
vec_mul(vc, va, vb);
vec_cmp<EQ>(vc, v_0);
vec_select(dst_intersection, v_0, v_1);
//向量求和
vec_cross_add(res1.data(), dst_a);
vec_cross_add(res2.data(), dst_b);
vec_cross_add(res_intersection.data(), dst_intersection);
问题:输入device端的数据必须是常量表达式,但是itk返回类型又不是常量。(申请的vector大小必须是已知的)
第三步:划分group成功。
第四步:
-
毕昇向量vector.load() 和 store() 完成后,原数组会发生转置。
-
之前数据出现乱码原因: memcpy() 是异步执行,必须得wait() 同步。Q.launch<>(){}前后使用Q.wait()同步
第五步:
使用vec_cmpv代替vec_cmp ,并用std::bitset<8>.count()方法代替select
bisheng::vector<uint8_t, unitLength / 8> cmpMask_a;
bisheng::vec_cmpv<GT>(cmpMask_a, va, v_0);
bisheng::vector<uint8_t, unitLength / 8> cmpMask_b;
bisheng::vec_cmpv<GT>(cmpMask_b, vb, v_0);
vec_mul(vc, va, vb);
bisheng::vector<uint8_t, unitLength / 8> cmpMask_c;
bisheng::vec_cmpv<GT>(cmpMask_c, vc, v_0);
for(int i = 0; i < unitLength / 8; ++i){
std::bitset<8> binary_a( cmpMask_a[i] );
int num_ones_a = binary_a.count();
std::bitset<8> binary_b( cmpMask_b[i] );
int num_ones_b = binary_b.count();
std::bitset<8> binary_c( cmpMask_c[i] );
int num_ones_c = binary_c.count();
res1[0] += num_ones_a;
res2[0] += num_ones_b;
res_intersection[0] += num_ones_c;
}
4.基于AscendC的MatmulPrelu融合算子开发
修改 AscendC的 MatmulLeakyRelu 工程样例,参考 Ascend 社区算子开发文档,利用矢量计算 API 实现 MatmulPrelu 融合算子开发。npu 仿真性能达到1500000tick。
AscendC端到端算子开发工程:
创建算子工程:
- 修改add_custom.json内容,报错算子输入输出的名字、数量和类型
- 利用msopgen生成创建算子工程
msopgen gen -i add_custom.json -c ai_core-<soc_version> -lan cpp -out $HOME/sample/AddCustom
- 编写
CMakePresets.json // 编译配置项
op_host // host侧实现文件 │ ├── add_custom_tiling.h // 算子tiling定义文件 │ ├── add_custom.cpp // 算子原型注册、shape推导、信息库、tiling实现等内容文件
op_kernel // kernel侧实现文件 │ ├── add_custom.cpp // 算子核函数实现文件
- op_kernel/add_custom.cpp // 算子核函数实现文件: 编写算子类、进行核函数的定义,并在核函数中调用算子类的Init和Process函数。
- op_host/add_custom_tiling.h // 算子tiling定义文件:实现数据切分和分块的过程。
- 在“add_custom.cpp”文件中实现算子的shape推导,多算子融合的话,对算子中间的Tensor shape以及dtype进行推导,1.可以提前为所有张量静态分配内存,避免动态内存分配带来的开销。2.可以让我们在图执行之前,就知道各Tensor的数据类型和形状,提前校验其正确性
- 进行算子原型注册。修改“add_custom.cpp”文件中的算子原型注册,此函数为入口函数。
算子工程编译部署
- 修改CMakePresets.json中ASCEND_CANN_PACKAGE_PATH为CANN软件包安装路径
- ./build.sh 进行算子工程编译。生成一个自定义算子安装包custom_opp_<target os>_<target architecture>.run
- 在自定义算子包所在路径下./custom_opp_<target os>_<target architecture>.run安装自定义算子
- 命令执行成功后,自定义算子包中的相关文件将部署至当前环境的OPP算子库的vendors/customize目录中,如果用户部署多个自定义算子包,可通过如下命令指定路径安装:./custom_opp_<target os>_<target architecture>.run --install-path=<path>
tiling注册与实现
标量运算,尽量在CPU侧执行。
数据对齐到一个data block(32B):
注册:
1.首先完成算子TilingData结构定义头文件的编写,该文件命名为“算子名称_tiling.h”
设置总计算量和每个核上总计算数据分块的个数
定义结构:
BEGIN_TILING_DATA_DEF(TilingData) // 注册一个tiling的类,以tiling的名字作为入参
TILING_DATA_FIELD_DEF(uint32_t, totalLength); // 添加tiling字段,总计算数据量
TILING_DATA_FIELD_DEF(uint32_t, tileNum); // 添加tiling字段,每个核上总计算数据分块个数
END_TILING_DATA_DEF;
注册TilingData结构:
// 注册算子tilingdata类到对应的AddCustom算子
REGISTER_TILING_DATA_CLASS(AddCustom, TilingData)
//第一个参数为op_type(算子类型),第二个参数为TilingData的类名
实现:
完成算子host实现cpp文件中Tiling函数实现,该文件命名为“算子名称.cpp”
tiling.h定义了TilingData的类,此时可以用TilingData定义一个具体的实例。
设置TilingData的totallength、tilenum等字段值。
// 用TilingData定义一个具体的实例
TilingData tiling;
// 设置TilingData
tiling.set_totalLength(totalLength);
tiling.set_tileNum(TILE_NUM);
通过调用TilingData类的SaveToBuffer接口完成TilingData的序列化和保存。
// 序列化并保存
tiling.SaveToBuffer(context->GetRawTilingData()->GetData(), context->GetRawTilingData()->GetCapacity());
context->GetRawTilingData()->SetDataSize(tiling.GetDataSize());
非对齐shape:
数据不对齐,有的核就要多算一个数据块(最小单位ALIGNNUM),
tingling结构就要配置好参数:分配较多数据量的核的总数据块数量
分配较少数据量的核的总数据块数量
分配较多数据量的核的总数据长度
分配较少数据量的核的总数据长度
定义和注册算子需要使用的tiling参数,
#ifndef ADD_CUSTOM_UNALIGN_TILING_H
#define ADD_CUSTOM_UNALIGN_TILING_H
#include "register/tilingdata_base.h"
namespace optiling {
BEGIN_TILING_DATA_DEF(TilingDataUnalign)
TILING_DATA_FIELD_DEF(uint32_t, formerNum); // 添加tiling字段,分配到较多数据量的核心数,即大块
TILING_DATA_FIELD_DEF(uint32_t, tailNum); // 添加tiling字段,分配到较少数据量的核心数,即小块
TILING_DATA_FIELD_DEF(uint32_t, formerLength); // 添加tiling字段,大块的长度
TILING_DATA_FIELD_DEF(uint32_t, tailLength); // 添加tiling字段,小块的长度
TILING_DATA_FIELD_DEF(uint32_t, alignNum); // 添加tiling字段,需要对齐到的最小数据量
END_TILING_DATA_DEF;
// 注册算子tilingdata类到对应的AddCustom算子
REGISTER_TILING_DATA_CLASS(AddCustomUnalign, TilingDataUnalign)
}
#endif // ADD_CUSTOM_UNALIGN_TILING_H
tiling实现,在“op_host/add_custom.cpp”中注册tiling实现接口。
namespace optiling {
constexpr uint32_t BLOCK_DIM = 8;
constexpr uint32_t SIZE_OF_HALF = 2;
constexpr uint32_t BLOCK_SIZE = 32;
// shape需要对齐到的最小单位
constexpr uint32_t ALIGN_NUM = BLOCK_SIZE / SIZE_OF_HALF;
static ge::graphStatus TilingFunc(gert::TilingContext *context)
{
TilingDataUnalign tiling;
uint32_t totalLength = context->GetInputTensor(0)->GetShapeSize();
context->SetBlockDim(BLOCK_DIM);
// 如果是非对齐的shape,需要向上对齐到最小单位
uint32_t totalLengthAligned = ((totalLength + ALIGN_NUM - 1) / ALIGN_NUM) * ALIGN_NUM;
// 把所有的数据尽可能均匀地分配到每个核上,如果不能均分的话,那么会有部分核多算一个最小单位ALIGN_NUM
// 通过模的计算,可以得到多算一个最小单位的核的数量,也可以得到少算一个最小单位的核的数量
// eg:1999 对齐后的总数据量为2000个数,核心数为8,数据块的最小单位是16,那么:
// 1、最小单位数据块的总数:2000 / 16 = 125
// 2、有5个核会分到16个最小单位的数据块:125 % 8 =5,可以称之为大块
// 3、有3个核会分到15个最小单位的数据块:8 - 5 = 3,可以称之为小块
uint32_t formerNum = (totalLengthAligned / ALIGN_NUM) % BLOCK_DIM;
uint32_t tailNum = BLOCK_DIM - formerNum;
// 计算大块和小块的数据量
uint32_t formerLength = ((totalLengthAligned / BLOCK_DIM + ALIGN_NUM - 1) / ALIGN_NUM) * ALIGN_NUM;//16*16=256
uint32_t tailLength = (totalLengthAligned / BLOCK_DIM / ALIGN_NUM) * ALIGN_NUM;//15*16=240
tiling.set_formerNum(formerNum);
tiling.set_tailNum(tailNum);
tiling.set_formerLength(formerLength);
tiling.set_tailLength(tailLength);
tiling.set_alignNum(ALIGN_NUM);
tiling.SaveToBuffer(context->GetRawTilingData()->GetData(), context->GetRawTilingData()->GetCapacity());
context->GetRawTilingData()->SetDataSize(tiling.GetDataSize());
context->SetTilingKey(1);
size_t *currentWorkspace = context->GetWorkspaceSizes(1);
currentWorkspace[0] = 0;
return ge::GRAPH_SUCCESS;
}
} // namespace optiling