香橙派5(RK3588)使用npu加速yolov5推理的部署过程
香橙派5使用npu加速yolov5推理的部署过程
硬件环境

部署过程
模型训练(x86主机)
在带nvidia显卡(最好)的主机上进行yolo的配置与训练, 获取最终的best.pt模型文件, 详见另一篇文档
模型转换(x86主机)
下载airockchip提供的yolov5(从pt到onnx)
一定要下这个版本的yolov5, 用于将pt模型转换成onnx模型, 这里的模型转换做了优化, 砍掉了最后一层, 只有这个onnx转换成rknn后, 可以正常推理
https://github.com/airockchip/yolov5.git
# 以下操作需要在虚拟环境中进行, 如果不是在虚拟环境 请先激活
cd yolov5
python3 export.py --rknpu --weight yolov5s.pt #请把这里的pt文件换成自己的文件
# 执行结束后, 应该会在当前目录下, 看到一个同名的 .onnx文件, 例如yolov5s.onnx
将该模型放到在线模型可视化网站, 检查模型的末端是否为下图类似的结构, 在sigmoid函数后直接就是输出, 而不是另一个检测层

如果是下面这种, 说明转换的onnx模型有问题, 用的export.py应该不是瑞芯微的, yolo官方支持的转换模型, 在最后多了一层检测, 正常是没有下面红框里的内容的

下载rknn-toolkit2(从onnx到rknn)
git clone https://github.com/airockchip/rknn-toolkit2.git
cd rknn-toolkit2/rknn-toolkit2/packages
# 需要确定是在虚拟环境下执行下面的指令, 如果不是在虚拟环境下, 请先使用下面的命令激活虚拟环境
# conda activate yolo
pip install -r requirements_cp38-2.2.0.txt #这里要对应python版本, 以及可能需要走清华镜像源, 否则很卡, 如果需要走镜像源, 请执行下面这段进行替代
# pip install -r requirements_cp38-2.2.0.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install rknn_toolkit2-2.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
如果有其他依赖不满足, 需要手动下载, 最后应该是这样的结果, 说明安装成功了

下载rknn_model_zoo
git clone https://github.com/airockchip/rknn_model_zoo.git
cd rknn_model_zoo/examples/yolov5/python
# 需要确定是在虚拟环境下执行下面的指令, 如果不是在虚拟环境下, 请先使用下面的命令激活虚拟环境
# conda activate yolo
python convert.py ../model/yolov5s_relu.onnx rk3588 i8 ../model/yolov5s_relu.rknn # 注意将前面的onnx模型路径修改成上面best.pt转换成的best.onnx路径, 后面那个是输出的路径, 可以随便改
香橙派部署(arm64)
下载虚拟环境管理工具 miniforge3
安装过程与在amd64上安装anaconda类似, 可以一路敲回车, 输yes
wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh
# 如果上一步卡很久, 可以考虑在自己电脑上挂梯子下好传过去, 或者走中转, 使用
# wget https://mirror.ghproxy.com/https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh 进行偷渡
chmod +x Miniforge3-Linux-aarch64.sh
./Miniforge3-Linux-aarch64.sh
# 一路回车就好, 默认安装路径为 ~/miniforge3
使用miniforge3创建虚拟环境
source ~/miniforge3/bin/activate #有可能在安装过程中, 就已经把这一步写进了环境变量, 看是否有(base)前缀, 有的话就不需要执行这一步

# 创建虚拟环境
conda create -n yolo python=3.8 #这里python版本可以指定不同的, 以3.8为例, -n 后面跟的是环境名
# 激活环境, 如果环境名不是yolo, 就把yolo这个名字换掉
conda activate yolo
安装rknn-toolkit-lite
git clone https://github.com/airockchip/rknn-toolkit2.git
# 以下命令需要在虚拟环境中进行
cd rknn-toolkit2/rknn-toolkit-lite2/packages
pip install rknn_toolkit_lite2-2.2.0-cp38-cp38-linux_aarch64.whl # 这一步可能会缺少相关依赖? 也许需要
# cd ../../rknn-toolkit2/packages
# pip install -r requirements_cp38-2.2.0.txt
测试推理(香橙派)
# 下载rknn_model_zoo
git clone https://github.com/airockchip/rknn_model_zoo.git
cd rknn_model_zoo/examples/yolov5/python
# 需要确定是在虚拟环境下执行下面的指令, 如果不是在虚拟环境下, 请先使用下面的命令激活虚拟环境
# conda activate yolo
修改香橙派自带的yolo代码
rknn-toolkit只支持amd64, 在香橙派上部署运行时需要arm架构的库, 即rknn-toolkit-lite, 因此相应的要修改该包的代码, 使用rknnlite进行推理
import os
import cv2
import sys
import argparse
import numpy as np
import platform
from rknnlite.api import RKNNLite# 添加路径
realpath = os.path.abspath(__file__)
_sep = os.path.sep
realpath = realpath.split(_sep)
sys.path.append(os.path.join(realpath[0]+_sep, *realpath[1:realpath.index('rknn_model_zoo')+1]))from py_utils.coco_utils import COCO_test_helperOBJ_THRESH = 0.25
NMS_THRESH = 0.45
IMG_SIZE = (640, 640) # (width, height), 例如 (1280, 736), 需要和训练时的size匹配CLASSES = ("pallet",) # 这里的类型需要匹配模型中检测的类型个数, 名称以及顺序, 否则会出现检测的是对的, 但是标签打错了的情况DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible'def get_host():# 获取平台和设备类型system = platform.system()machine = platform.machine()os_machine = system + '-' + machineif os_machine == 'Linux-aarch64':try:with open(DEVICE_COMPATIBLE_NODE) as f:device_compatible_str = f.read()if 'rk3588' in device_compatible_str:host = 'RK3588'else:host = 'RK356x'except IOError:print('Read device node {} failed.'.format(DEVICE_COMPATIBLE_NODE))exit(-1)else:host = os_machinereturn hostdef filter_boxes(boxes, box_confidences, box_class_probs):box_confidences = box_confidences.reshape(-1)class_max_score = np.max(box_class_probs, axis=-1)classes = np.argmax(box_class_probs, axis=-1)_class_pos = np.where(class_max_score * box_confidences >= OBJ_THRESH)scores = (class_max_score * box_confidences)[_class_pos]boxes = boxes[_class_pos]classes = classes[_class_pos]return boxes, classes, scoresdef nms_boxes(boxes, scores):x = boxes[:, 0]y = boxes[:, 1]w = boxes[:, 2] - boxes[:, 0]h = boxes[:, 3] - boxes[:, 1]areas = w * horder = scores.argsort()[::-1]keep = []while order.size > 0:i = order[0]keep.append(i)xx1 = np.maximum(x[i], x[order[1:]])yy1 = np.maximum(y[i], y[order[1:]])xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)inter = w1 * h1ovr = inter / (areas[i] + areas[order[1:]] - inter)inds = np.where(ovr <= NMS_THRESH)[0]order = order[inds + 1]keep = np.array(keep)return keepdef post_process(input_data, anchors):boxes, scores, classes_conf = [], [], []# 调试输出 - 查看 input_data 和 anchors 的形状for i in range(len(input_data)):print(f"Debug: input_data[{i}].shape = {input_data[i].shape}")print(f"Debug: anchors[{i}] = {anchors[i]}")# 调整 reshape 逻辑以匹配实际输入尺寸for i in range(len(input_data)):# 根据输出形状和锚点数调整 reshape 逻辑num_anchors = len(anchors[i])grid_h, grid_w = input_data[i].shape[-2], input_data[i].shape[-1]expected_channels = num_anchors * (5 + len(CLASSES))if input_data[i].shape[1] != expected_channels:print(f"Error: input_data[{i}] channels {input_data[i].shape[1]} does not match expected channels {expected_channels}")continue# Reshape to (num_anchors, 5 + num_classes, grid_h, grid_w)input_data[i] = input_data[i].reshape((num_anchors, 5 + len(CLASSES), grid_h, grid_w))boxes.append(box_process(input_data[i][:, :4, :, :], anchors[i]))scores.append(input_data[i][:, 4:5, :, :])classes_conf.append(input_data[i][:, 5:, :, :])def sp_flatten(_in):ch = _in.shape[1]_in = _in.transpose(0, 2, 3, 1)return _in.reshape(-1, ch)boxes = [sp_flatten(_v) for _v in boxes]classes_conf = [sp_flatten(_v) for _v in classes_conf]scores = [sp_flatten(_v) for _v in scores]boxes = np.concatenate(boxes) if boxes else np.array([])classes_conf = np.concatenate(classes_conf) if classes_conf else np.array([])scores = np.concatenate(scores) if scores else np.array([])if len(boxes) == 0 or len(classes_conf) == 0 or len(scores) == 0:return None, None, Noneboxes, classes, scores = filter_boxes(boxes, scores, classes_conf)nboxes, nclasses, nscores = [], [], []for c in set(classes):inds = np.where(classes == c)b = boxes[inds]c = classes[inds]s = scores[inds]keep = nms_boxes(b, s)if len(keep) != 0:nboxes.append(b[keep])nclasses.append(c[keep])nscores.append(s[keep])if not nclasses and not nscores:return None, None, Noneboxes = np.concatenate(nboxes)classes = np.concatenate(nclasses)scores = np.concatenate(nscores)return boxes, classes, scoresdef box_process(position, anchors):grid_h, grid_w = position.shape[2:4]anchors = np.array(anchors)num_anchors = len(anchors)anchors = anchors.reshape(num_anchors, 2, 1, 1)# 生成网格col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h))grid = np.stack((col, row), axis=0).astype(np.float32)grid = grid[np.newaxis, :, :, :] # shape: (1, 2, grid_h, grid_w)# 位置解码box_xy = (position[:, :2, :, :] * 2.0 - 0.5 + grid) * (IMG_SIZE[0] / grid_w)box_wh = (position[:, 2:4, :, :] * 2.0) ** 2 * anchors# 将 [c_x, c_y, w, h] 转换为 [x1, y1, x2, y2]box = np.concatenate((box_xy - box_wh / 2.0, box_xy + box_wh / 2.0), axis=1)return boxdef draw(image, boxes, scores, classes):for box, score, cl in zip(boxes, scores, classes):left, top, right, bottom = [int(_b) for _b in box]print("%s @ (%d %d %d %d) %.3f" % (CLASSES[cl], left, top, right, bottom, score))cv2.rectangle(image, (left, top), (right, bottom), (255, 0, 0), 2)cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),(left, top - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)def setup_model(args):# 根据设备类型选择模型if args.model_path:rknn_model = args.model_pathelse:rknn_model = 'pallet_detection.rknn' # Replace with your model pathrknn_lite = RKNNLite()ret = rknn_lite.load_rknn(rknn_model)if ret != 0:print('Load RKNN model failed')exit(ret)print('done')return rknn_liteif __name__ == '__main__':parser = argparse.ArgumentParser(description='RKNNLite inference example')parser.add_argument('--model_path', type=str, required=False, help='model path, could be .rknn file')parser.add_argument('--target', type=str, required=False, default='rk3566', help='target RKNPU platform')parser.add_argument('--img_folder', type=str, default='../model', help='img folder path')parser.add_argument('--img_show', action='store_true', default=False, help='draw the result and show')parser.add_argument('--img_save', action='store_true', default=False, help='save the result')parser.add_argument('--coco_map_test', action='store_true', help='enable coco map test')args = parser.parse_args()# 初始化模型rknn_lite = setup_model(args)print('--> Init runtime environment')ret = rknn_lite.init_runtime()if ret != 0:print('Init runtime environment failed')exit(ret)print('done')# 加载图片file_list = sorted(os.listdir(args.img_folder))img_list = [f for f in file_list if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]co_helper = COCO_test_helper(enable_letter_box=True)# 定义锚点(这里用的是yolo默认的锚点, 可以使用自定义训练后生成的)anchors = [[[10, 13], [16, 30], [33, 23]], # 第一个尺度的锚点[[30, 61], [62, 45], [59, 119]], # 第二个尺度的锚点[[116, 90], [156, 198], [373, 326]] # 第三个尺度的锚点]# 运行推理for i, img_name in enumerate(img_list):print('infer {}/{}'.format(i + 1, len(img_list)), end='\r')img_path = os.path.join(args.img_folder, img_name)img_src = cv2.imread(img_path)if img_src is None:continueimg = co_helper.letter_box(im=img_src.copy(), new_shape=(IMG_SIZE[1], IMG_SIZE[0]), pad_color=(0, 0, 0))img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)img = img[np.newaxis, ...] # 增加批次维度以匹配4D输入要求# 推理outputs = rknn_lite.inference(inputs=[img])# 输出调试信息for idx, output in enumerate(outputs):print(f"Output[{idx}] shape: {output.shape}")boxes, classes, scores = post_process(outputs, anchors=anchors)if args.img_show or args.img_save:print('\n\nIMG: {}'.format(img_name))img_p = img_src.copy()if boxes is not None:draw(img_p, co_helper.get_real_box(boxes), scores, classes)if args.img_save:result_path = os.path.expanduser(f'~/result_{i+1}.jpg')cv2.imwrite(result_path, img_p)print('Detection result save to {}'.format(result_path))if args.img_show:cv2.imshow("full post process result", img_p)cv2.waitKeyEx(0)rknn_lite.release()
运行推理
# 以下命令需要在虚拟环境中进行, 当前应该在以下目录 rknn_model_zoo/examples/yolov5/python
python3 pallet_detect.py --model_path ~/new.rknn --img_folder images/ --img_save
# 修改model_path为新模型的实际目录
# 修改img_folder为实际要测试的图像集
# 如果需要保存图像, 则加上--img_save, 会将图片保存到当前路径下, 以result_*jpg为名
相关文章:
香橙派5(RK3588)使用npu加速yolov5推理的部署过程
香橙派5使用npu加速yolov5推理的部署过程 硬件环境 部署过程 模型训练(x86主机) 在带nvidia显卡(最好)的主机上进行yolo的配置与训练, 获取最终的best.pt模型文件, 详见另一篇文档 模型转换(x86主机) 下载airockchip提供的yolov5(从pt到onnx) 一定要下这个版本的yolov5, …...
基于MWORKS的蓝桥杯「智能装备数字化建模大赛」正式发布,首期培训本周六开启
为强化装备数字化人才培养,推动装备数字化技术快速发展,第十六届蓝桥杯全国软件和信息技术专业人才大赛设置专项赛暨智能装备数字化建模大赛,使用MWORKS作为参赛软件。关于参赛软件授权、技术支持与培训、教材与案例开发支持、成果转化培训及…...
021、深入解析前端请求拦截器
目录 深入解析前端请求拦截器: 1. 引言 2. 核心实现与基础概念 2.1 基础拦截器实现 2.2 响应拦截器配置 3. 实际应用场景 3.1 完整的用户认证系统 3.2 文件上传系统 3.3 API请求缓存系统 3.4 请求重试机制 3.5 国际化处理 4. 性能优化实践 4.1 请求合并…...
windows中的tracert命令
在 Windows 操作系统中,tracert(全称 Trace Route)是一个用于确定 IP 数据包到达目标主机所经过的路径的命令行工具。它通过发送具有不同生存时间(TTL)的 ICMP(Internet Control Message Protocolÿ…...
【玩儿】Java 数字炸弹小游戏(控制台版)+ IO 数据存储
Java 数字炸弹小游戏(控制台版) IO 数据存储 数字炸弹小游戏概述功能实现实体类User.java 玩家信息实体类GameRecode.java 游戏记录实体类 自定义异常AccountLockedException.java 账号锁定异常PasswordErrorException.java 密码错误异常UnknowAccountEx…...
今日头条躺赚流量:自动化新闻爬取和改写脚本
构建一个自动化的新闻爬取和改写系统,实现热点新闻的自动整理和发布,需要分为以下几个模块:新闻爬取、信息解析与抽取、内容改写、自动发布。以下是每个模块的详细实现步骤和代码示例: 1. 新闻爬取模块 目标:从新闻网…...
日常实习与暑期实习详解
日常实习与暑期实习详解 问了下正在实习的同学,发现天要塌了–才知道日常实习是没有笔试的 1. 实习的定义 1.1 日常实习 日常实习是企业长期招聘的实习岗位,通常没有时间限制。企业会在需要时进行招聘,招聘对象包括在校大学生和大一、大二的…...
Git的原理和使用(六)
本文主要讲解企业级开发模型 1. 引入 交付软件的流程:开发->测试->发布上线 上面三个过程可以详细划分为一下过程:规划、编码、构建、测试、发 布、部署和维护 最初,程序⽐较简单,⼯作量不⼤,程序员⼀个⼈可以完…...
Elasticsearch 中的高效按位匹配
作者:来自 Elastic Alexander Marquardt 探索在 Elasticsearch 中编码和匹配二进制数据的六种方法,包括术语编码(我喜欢的方法)、布尔编码、稀疏位位置编码、具有精确匹配的整数编码、具有脚本按位匹配的整数编码以及使用 ESQL 进…...
LSTM,全称长短期记忆网络(Long Short-Term Memory),是一种特殊的循环神经网络(RNN)结构
关于lstm超参数设置,每个参数都有合适的范围,超过这个范围则lstm训练不再有效,loss不变,acc也不变 LSTM,全称长短期记忆网络(Long Short-Term Memory),是一种特殊的循环神经网络&am…...
导出问题处理
问题描述 测试出来一个问题,使用地市的角色,导出数据然后超过了20w的数据,提示报错,我还以为是偶然的问题,然后是发现是普遍的问题,本地环境复现了,然后是,这个功能是三套角色&…...
通过cv库智能切片 把不同的分镜切出来 自媒体抖音快手混剪
用 手机自动化脚本,从自媒体上获取视频,一个商品对应几百个视频,我们把这几百个视频下载下来,进行分镜 视频切片,从自媒体上下载视频,通过cv库用直方图识别每个镜头进行切片。 下载多个图片进行视频的伪原…...
【机器学习】——numpy教程
文章目录 1.numpy简介2.初始化numpy3.ndarry的使用3.1numpy的属性3.2numpy的形状3.3ndarray的类型 4numpy生成数组的方法4.1生成0和1数组4.2从现有的数组生成4.3生成固定范围的数组4.4生成随机数组 5.数组的索引、切片6.数组的形状修改7.数组的类型修改8.数组的去重9.ndarray的…...
多线程——线程的状态
线程状态的意义 线程状态的意义在于描述线程在执行过程中的不同阶段和条件,帮助开发者更好地管理和调度线程资源。 线程的多种状态 线程的状态是一个枚举类型(Thread.State),可以通过线程名.getState()…...
开源数据库 - mysql - 组织结构(与oracle的区别)
组织形式区别 mysql(Schema -> Table -> Column -> Row) Schema(方案): Scheme是关于数据库和表的布局及特性的信息。它可以用来描述数据库中特定的表以及整个数据库和其中表的信息,如表的一些特…...
vue3+vite 部署npm 包
公司需要所以研究了一下怎么部署安装,比较简单 先下载个vue项目 不用安准路由,pinna 啥的,只需要一个最简单的模版 删掉App.vue 中的其它组件 npm create vuelatest 开始写自定义组件 新建一个el-text 组件, name是重点,vue3中…...
华为鸿蒙HarmonyOS应用开发者高级认证视频及题库答案
华为鸿蒙开发者高级认证的学习资料 1、课程内容涵盖HarmonyOS系统介绍、DevEco Studio工具使用、UI设计与开发、Ability设计与开发、分布式特性、原子化服务卡片以及应用发布等。每个实验都与课程相匹配,帮助加深理解并掌握技能 2、学习视频资料 华为HarmonyOS开发…...
【计网】从零开始认识IP协议 --- 认识网络层,认识IP报头结构
从零开始认识IP协议 1 网络层协议1.1 初步认识IP协议1.2 初步理解IP地址 2 IP协议报头3 初步理解网段划分 1 网络层协议 1.1 初步认识IP协议 我们已经熟悉了传输层中的UDP和TCP协议,接下来我们来接触网络层的协议: 网络层在计算机网络中的意义主要体现…...
大一物联网要不要转专业,转不了该怎么办?
有幸在2014年,踩中了物联网的风口,坏消息,牛马的我,一口汤都没喝上。 依稀记得,当时市场部老大,带我去上海参加电子展会,印象最深的,一些物联网云平台,靠着一份精美PPT&a…...
LeetCode题练习与总结:4的幂--342
一、题目描述 给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。 整数 n 是 4 的幂次方需满足:存在整数 x 使得 n 4^x 示例 1: 输入:n 16 输出&am…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...
