CANN ops-cv CIoU 算子全解析:从损失函数原理到 aclnnCIoU 两段式接口实战
【免费下载链接】ops-cv本项目是CANN提供的图像处理、目标检测相关的算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-cv
本篇技术指南围绕 CANN ops-cv 算子库中的CIoU 算子展开,完整讲解其在边界框回归中的数学原理(在 IoU 基础上同时引入中心点距离、宽高比与重叠面积的惩罚项)、Ascend 950 系列产品的支持情况、全部输入/输出/属性参数与约束条件,并结合仓库源码剖析aclnnCIoUGetWorkspaceSize的入参校验逻辑与底层计算流程,最后给出可直接复用的aclnnCIoU两段式接口调用示例。读完本文,你将能够独立完成 CIoU 算子在 NPU 上的算子调用、参数配置与结果验证。
一、CIoU 算子功能与数学原理
CIoU(Complete IoU)算子是用于边界框回归的损失函数。它在 IoU 的基础上同时考虑了中心点距离、宽高比和重叠面积三个维度,从而更全面地衡量预测框(bBox)与真实框(gtBox)之间的差异。相比单纯使用 IoU,CIoU 能够在预测框与真实框完全没有重叠(IoU 为 0)时依然提供有效的梯度信号,加速回归收敛。
在 objdetect/ciou/README.md 中给出的计算公式如下:
$$ CIoU = IoU - \frac{\rho^2(b^p, b^g)}{c^2} - \alpha v \ v = \frac{4}{\pi^2}(arctan(\frac{w^g}{h^g}) - arctan(\frac{w^p}{h^p}))^2 \ \alpha = \frac{v}{1 - IoU + v} $$
其中各符号含义为:
| 符号 | 含义 |
|---|---|
IoU | 预测框与真值框的交并比 |
ρ²(bᵖ, bᵍ) | 预测框中心点与真值框中心点之间欧氏距离的平方 |
c² | 同时包含预测框与真值框的最小外接矩形对角线长度的平方 |
v | 衡量预测框与真值框宽高比一致性的惩罚项,基于两者宽高比的arctan差值 |
α | 平衡因子,用于对v进行归一化,当 IoU 越大时α越趋向于 1,宽高比惩罚越显著 |
从公式可以看出,CIoU 在 IoU 基础上做了两项修正:ρ²/c²惩罚中心点偏移,αv惩罚宽高比不一致。同时该算子支持mode属性切换iou(交并比)与iof(前景交叉比,intersection over foreground)两种计算方式,并额外输出计算过程中的atanSub(两个arctan的差值),便于上层在需要时复用它构造其他变体损失。
二、产品支持情况
依据 objdetect/ciou/README.md 与 aclnnCIoU 接口文档 的产品支持矩阵,CIoU 算子的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | √ |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | × |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | × |
| Atlas 200I/500 A2 推理产品 | × |
| Atlas 推理系列产品 | × |
| Atlas 训练系列产品 | × |
适用前提:当前仓库中该算子仅在RegBase(寄存器基址)平台实现,源码 op_api/aclnn_ciou.cpp 中的CheckSocValid()通过IsRegBase()判断当前 SoC 版本是否受支持,非 RegBase 平台会直接返回ACLNN_ERR_RUNTIME_ERROR。因此在非 Ascend 950 系列平台上调用aclnnCIoU会失败,请务必以实际运行硬件为准。
三、算子参数说明
3.1 顶层算子参数
CIoU 算子的完整参数定义见 objdetect/ciou/README.md 参数说明表:
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
bboxes | 输入 | 预测矩形框 | FLOAT32、FLOAT16 | ND |
gtboxes | 输入 | 真值矩形框 | FLOAT32、FLOAT16 | ND |
trans | 属性 | 用于指定矩形框的格式 | BOOL | - |
is_cross | 属性 | 用于指定 bBoxes 与 gtBoxes 之间是否进行交叉运算 | BOOL | - |
mode | 属性 | 用于选择计算方式"iou"或"iof" | String | - |
atan_sub_flag | 属性 | 用于指定是否输出 atan_sub | BOOL | - |
overlap | 输出 | 根据两个输入计算得到的交并比/前景交叉比 | FLOAT32、FLOAT16 | ND |
atan_sub | 输出 | 计算过程中两个 arctan 的差值 | FLOAT32、FLOAT16 | ND |
各属性的取值语义(结合 aclnn_ciou.h 的接口注释整理):
- trans:
true表示输入矩形框格式为[x, y, w, h](中心点坐标 + 宽高);false表示输入格式为[x0, y0, x1, y1](左上角 + 右下角坐标)。 - is_cross:是否对 bBoxes 与 gtBoxes 做交叉运算。
true时输出 shape 为[M, N];false时输出 shape 为[1, N]。当前版本仅支持false。 - mode:
"iou"计算交并比;"iof"计算前景交叉比(intersection over foreground,即交集面积除以 bBox 面积)。 - atan_sub_flag:是否输出
atanSub张量。当前版本仅支持true(即必须输出两个 arctan 的差值)。
3.2 接口层参数(aclnnCIoUGetWorkspaceSize)
在 aclnn 接口层,属性参数直接作为函数入参传递,接口原型参见 aclnnCIoU.md 与 aclnn_ciou.h:
aclnnStatus aclnnCIoUGetWorkspaceSize( const aclTensor *bBoxes, // 预测矩形框,[4, M],FLOAT32/FLOAT16,ND,支持非连续 const aclTensor *gtBoxes, // 真值矩形框,[4, N],FLOAT32/FLOAT16,ND,支持非连续 bool trans, // true: [x,y,w,h];false: [x0,y0,x1,y1] bool isCross, // 是否交叉运算,当前仅支持 false const char *mode, // "iou" 或 "iof" aclTensor *overlap, // 输出,[1, N],与输入同 dtype,支持非连续 aclTensor *atanSub, // 输出,[1, N],与输入同 dtype,支持非连续 uint64_t *workspaceSize, // 输出,需要申请的 workspace 大小 aclOpExecutor **executor); // 输出,op 执行器,包含算子计算流程 aclnnStatus aclnnCIoU( void *workspace, // Device 侧 workspace 内存地址 uint64_t workspaceSize, // 由第一段接口获取 aclOpExecutor *executor, // 第一段接口返回的执行器 aclrtStream stream); // 指定执行任务的 Stream接口层各参数要点(源自 aclnnCIoU.md 参数说明表):
bBoxes:形状为[4, M]的二维 Tensor,第一维固定为 4(分别对应 x/y/w/h 或 x0/y0/x1/y1 四个坐标分量)。gtBoxes:形状为[4, N]的二维 Tensor。overlap/atanSub:输出 Tensor,形状为[1, N](isCross=false时),数据类型、数据格式需与输入保持一致。- 输入输出均支持非连续 Tensor(
√标记),接口内部会自动做Contiguous转换。 mode在接口层类型为CHAR*(对应顶层属性表的 String)。
四、约束说明
CIoU 算子的使用约束(README 与 aclnn 接口文档一致,且被源码校验逻辑印证):
- 坐标有效性:若输入格式为
[x0, y0, x1, y1],其中(x0, y0)和(x1, y1)分别表示矩形框的左上角和右下角,必须满足x1 > x0, y1 > y0,否则矩形框无效。 - M 和 N 需要一致:即
bBoxes与gtBoxes第二维必须相等(在isCross=false场景下)。 is_cross目前仅支持false:传true会在参数校验阶段报错。atan_sub_flag目前仅支持true:即atanSub输出张量必须提供。- 确定性计算:
aclnnCIoU默认采用确定性实现,相同输入多次执行结果一致。
上述约束在源码中有完整对应,例如 aclnn_ciou.cpp 中定义了三个关键常量:
static const int64_t TENSOR_DIM_NUM = 2; // 输入输出必须为二维 static const int64_t INPUT_FIRST_DIM = 4; // 输入第一维必须为 4 static const int64_t INPUT_SECOND_DIM_CONSTRAINT = 1024; // 输入第二维必须是 1024 的倍数其中"第二维必须是 1024 的倍数"是 aclnn 接口层相对 README 的进一步补充约束(与示例中[4, 1024]的 shape 对应),在实际调用时同样需要满足,否则返回ACLNN_ERR_PARAM_INVALID。
五、源码级实现解析
5.1 两段式接口与底层计算流程
aclnnCIoU采用 CANN 标准的两段式接口设计:先调用aclnnCIoUGetWorkspaceSize完成入参校验、计算 workspace 大小并生成执行器,再调用aclnnCIoU真正执行计算。
在 aclnn_ciou.cpp 的第一段接口实现中,核心流程依次为:
- 平台校验:
CheckSocValid()检查当前 SoC 是否属于 RegBase(Ascend 950)平台。 - 参数校验:
CheckParams()按顺序执行CheckNotNull(空指针检查)、CheckDtypeValid(数据类型与格式检查)、CheckAttr(属性检查)、CheckShape(shape 检查)。 - 空 Tensor 短路:
bBoxes或gtBoxes为空 Tensor 时,workspace 直接置 0 并返回成功。 - 连续化处理:通过
l0op::Contiguous将可能的非连续输入转换为连续 Tensor。 - 底层算子调用:
l0op::CIoU(bBoxesContiguous, gtBoxesContiguous, trans, isCross, mode, true, ...)构建计算图,其中最后一个参数true即对应atanSubFlag。 - 输出搬运:通过两次
l0op::ViewCopy将内部计算结果写入用户提供的overlap与atanSub张量(兼容非连续输出)。 - 获取 workspace 大小:
executor->GetWorkspaceSize()返回计算所需 workspace 字节数。
5.2 校验逻辑的代码佐证
- 空指针检查aclnn_ciou.cpp:
bBoxes、gtBoxes、overlap、atanSub任一为空即返回ACLNN_ERR_PARAM_NULLPTR。 - 数据类型检查aclnn_ciou.cpp:四个张量数据类型必须一致,且均须属于支持列表
{DT_FLOAT, DT_FLOAT16};格式必须全部为FORMAT_ND,否则返回ACLNN_ERR_PARAM_INVALID。 - 属性检查aclnn_ciou.cpp:
mode非空且只能为"iou"或"iof";isCross必须为false。 - shape 检查aclnn_ciou.cpp:输入输出必须为二维;输入第一维必须为 4;输入第二维必须是 1024 的倍数;
isCross=false时输出第一维必须为 1 且各张量第二维相等;isCross=true时输出 shape 应为[M, N]。
5.3 输出 shape 的推导
在 op_api/ciou.cpp 中可以看到输出 shape 的推导逻辑:
if (isCross == false) { outShape = {1, bBoxes->GetViewShape().GetDim(1)}; } else { outShape = {gtBoxes->GetViewShape().GetDim(1), bBoxes->GetViewShape().GetDim(1)}; }即isCross=false时输出为[1, N](与gtBoxes第二维一致);isCross=true时输出为[M, N](M 为 gtBoxes 数量、N 为 bBoxes 数量)。随后通过ADD_TO_LAUNCHER_LIST_AICORE(CIoU, ...)将算子挂载到AICore执行器列表,说明 CIoU 计算在 NPU 的 AI Core 上执行。
六、aclnnCIoU 调用示例与步骤拆解
6.1 完整示例代码
仓库提供了可直接参考的调用样例 examples/test_aclnn_ciou.cpp,完整代码(与 aclnnCIoU.md 调用示例一致)如下:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_ciou.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vector<int64_t>& shape) { int64_t shape_size = 1; for (auto i : shape) { shape_size *= i; } return shape_size; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,AscendCL初始化 auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } template <typename T> int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { strides[i] = shape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. (固定写法)device/stream初始化,参考AscendCL对外接口列表 int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出 std::vector<float> bBoxesHostData(4096, 1); std::vector<float> gtBoxesHostData(4096, 2); std::vector<float> overlapHostData(1024, 0); std::vector<float> atanSubHostData(1024, 0); std::vector<int64_t> bBoxesShape = {4, 1024}; std::vector<int64_t> gtBoxesShape = {4, 1024}; std::vector<int64_t> overlapShape = {1, 1024}; std::vector<int64_t> atanSubShape = {1, 1024}; void* bBoxesDeviceAddr = nullptr; void* gtBoxesDeviceAddr = nullptr; void* overlapDeviceAddr = nullptr; void* atanSubDeviceAddr = nullptr; aclTensor* bBoxes = nullptr; aclTensor* gtBoxes = nullptr; aclTensor* overlap = nullptr; aclTensor* atanSub = nullptr; ret = CreateAclTensor(bBoxesHostData, bBoxesShape, &bBoxesDeviceAddr, aclDataType::ACL_FLOAT, &bBoxes); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(gtBoxesHostData, gtBoxesShape, >BoxesDeviceAddr, aclDataType::ACL_FLOAT, >Boxes); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(overlapHostData, overlapShape, &overlapDeviceAddr, aclDataType::ACL_FLOAT, &overlap); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(atanSubHostData, atanSubShape, &atanSubDeviceAddr, aclDataType::ACL_FLOAT, &atanSub); CHECK_RET(ret == ACL_SUCCESS, return ret); // attr bool trans = false; bool isCross = false; const char* mode = "iou"; uint64_t workspaceSize = 0; aclOpExecutor* executor; // 3. 调用CANN算子库API(第一段接口) ret = aclnnCIoUGetWorkspaceSize(bBoxes, gtBoxes, trans, isCross, mode, overlap, atanSub, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnCIoUGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } // 4. 第二段接口:执行计算 ret = aclnnCIoU(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnCIoU failed. ERROR: %d\n", ret); return ret); // 5. (固定写法)同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 6. 获取输出值:将device侧内存上的结果拷贝至host侧 auto overlapSize = GetShapeSize(overlapShape); std::vector<float> overlapData(overlapSize, 0); ret = aclrtMemcpy(overlapData.data(), overlapData.size() * sizeof(overlapData[0]), overlapDeviceAddr, overlapSize * sizeof(overlapData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy overlapData from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < overlapSize; i++) { LOG_PRINT("overlap[%ld] is: %f\n", i, overlapData[i]); } auto atanSubsize = GetShapeSize(atanSubShape); std::vector<float> atanSubData(atanSubsize, 0); ret = aclrtMemcpy(atanSubData.data(), atanSubData.size() * sizeof(atanSubData[0]), atanSubDeviceAddr, atanSubsize * sizeof(atanSubData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy atanSubData from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < atanSubsize; i++) { LOG_PRINT("atanSub[%ld] is: %f\n", i, atanSubData[i]); } // 7. 释放aclTensor aclDestroyTensor(bBoxes); aclDestroyTensor(gtBoxes); aclDestroyTensor(overlap); aclDestroyTensor(atanSub); // 8. 释放device资源 aclrtFree(bBoxesDeviceAddr); aclrtFree(gtBoxesDeviceAddr); aclrtFree(overlapDeviceAddr); aclrtFree(atanSubDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }6.2 调用步骤拆解
对照源码将整个调用流程拆解为八个固定步骤,可套用到绝大多数 aclnn 算子:
- 环境初始化:
aclInit→aclrtSetDevice→aclrtCreateStream,完成 AscendCL 运行环境与 Stream 的创建。 - 构造输入输出 Tensor:通过
aclrtMalloc申请 Device 侧内存、aclrtMemcpy拷贝 Host 数据、aclCreateTensor创建aclTensor。示例中 shape 取bBoxes=[4,1024]、gtBoxes=[4,1024]、overlap=[1,1024]、atanSub=[1,1024],满足"第一维为 4、第二维为 1024 的倍数、M=N"等全部约束。 - 调用第一段接口
aclnnCIoUGetWorkspaceSize:完成入参校验、生成执行器并返回 workspace 大小。 - 申请 workspace:仅当
workspaceSize > 0时用aclrtMalloc申请对应大小的 Device 内存。 - 调用第二段接口
aclnnCIoU:传入 workspace、执行器与 Stream,完成实际计算。 - 同步等待:
aclrtSynchronizeStream确保算子任务执行完成。 - 结果回读:通过
aclrtMemcpy(..., ACL_MEMCPY_DEVICE_TO_HOST)将overlap与atanSub从 Device 拷贝到 Host 并打印。 - 资源释放:依次销毁
aclTensor、释放 Device 内存与 workspace、销毁 Stream、复位 Device、aclFinalize。
编译与运行样例的完整过程可参考 编译与运行样例。示例代码中bBoxes全为 1、gtBoxes全为 2,此时矩形框宽高均为 1,中心点重合,IoU = 1,因此预期overlap输出为 1、atanSub输出为 0,可作为功能验证的参考预期值。
七、返回值与错误码
aclnnCIoUGetWorkspaceSize与aclnnCIoU均返回aclnnStatus状态码,完整返回码定义参见 aclnn 返回码。第一段接口完成入参校验,出现以下场景时报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
ACLNN_ERR_PARAM_NULLPTR | 161001 | bBoxes、gtBoxes、overlap或atanSub是空指针 |
ACLNN_ERR_PARAM_INVALID(多场景) | 161002 | bBoxes、gtBoxes、overlap、atanSub不是二维;四个张量数据类型不一致;数据类型或数据格式不在支持范围内;bBoxes或gtBoxes第一维不是 4;第二维不是 1024 的倍数;overlap或atanSub第一维不是 1;四个张量第二维不相等;isCross不是false;mode不是"iou"或"iof" |
这些错误码对应关系与 aclnn_ciou.cpp 中CheckParams的校验链一一对应,出现161002时可通过排查上述列表快速定位问题参数。
八、单元测试对约束的验证
仓库在 tests/ut/op_host/op_api/test_aclnn_ciou.cpp 中提供了完整的 gtest 单元测试,覆盖了正常路径与异常路径:
- 成功用例:
bBoxes=[4,1024]、gtBoxes=[4,1024]、overlap=[1,1024]、atanSub=[1,1024],trans=false, isCross=false, mode="iou",在SocVersion::ASCEND950平台上调用第一段接口返回ACLNN_SUCCESS。 - 空指针用例:分别将
bBoxes、gtBoxes、overlap、atanSub置空,期望返回ACLNN_ERR_PARAM_NULLPTR。 - 非法 dtype 用例:输出使用
ACL_INT64、ACL_BF16或输入使用ACL_BF16、输入输出 dtype 不一致,期望返回ACLNN_ERR_PARAM_INVALID。 - 非法 format 用例:任一张量使用
ACL_FORMAT_NCHW或ACL_FORMAT_FRACTAL_NZ,期望返回ACLNN_ERR_PARAM_INVALID。 - 非法 shape 用例:输出第一维非 1、输入第一维非 4、输入第二维非 1024 倍数、输出第二维与输入不一致、维度数非二维等,均期望返回
ACLNN_ERR_PARAM_INVALID。 - 非法 attr 用例:
isCross=true、mode="iouf",期望返回ACLNN_ERR_PARAM_INVALID。
这些用例从测试侧印证了第五节源码校验逻辑的每一条规则,是排查调用问题时的最佳参照。
九、总结与最佳实践
CIoU 算子是 CANN ops-cv 在边界框回归场景下提供的高阶损失算子,通过aclnnCIoU两段式接口即可在 Ascend 950 系列 NPU 上完成计算。使用时的关键要点:
- 确认硬件平台:仅 Ascend 950PR / Ascend 950DT 支持,其他系列调用会直接失败。
- 严格遵循 shape 约束:输入
[4, M]、输出[1, N],M 与 N 一致,第二维为 1024 的倍数。 - 属性取值固定:
isCross传false,mode只能取"iou"或"iof",同时记得提供atanSub输出张量。 - 遵循两段式调用范式:先取 workspace 与 executor,再申请内存并执行,最后同步、回读、释放。
- 借助错误码排查:
161001查空指针,161002对照校验链逐项排查 dtype、format、shape 与 attr。
文中涉及的算子文档、接口实现、示例代码与单元测试均可在仓库objdetect/ciou/目录下找到,推荐在动手开发前通读 README.md 与 aclnnCIoU.md 两份文档,并结合本文的源码解析理解其内部校验与计算流程。
【免费下载链接】ops-cv本项目是CANN提供的图像处理、目标检测相关的算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-cv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考