本教程以 tiny-yolov3 模型为例,介绍如何在边缘智能创建自定义推理模型,并在边缘智能节点(一体机)上部署相应的模型服务。此外,本教程提供了一份示例代码,可用于验证模型服务是否正常工作。
在边缘智能创建自定义模型前,您需要准备好模型文件及相关的配置信息。
下载模型文件。
通过 GitHub 获取所需的模型文件。访问 tiny-yolov3-11.onnx 模型页面,然后单击下载图标,下载模型文件。
调整文件结构。
边缘智能对模型文件的文件结构有特定要求,因此在下载模型文件后,您需要按照要求调整文件的结构。
1
。tiny-yolov3-11.onnx
重命名为model.onnx
,然后将它移动到文件夹1
。1
就成为符合边缘智能要求的模型文件。获取模型的输入和输出属性。
创建自定义模型时,您需要提供模型的输入和输出配置。您可以通过以下方式获取所需信息:
阅读模型的文档,从文档中获取输入和输出属性。
(推荐)使用 Netron 工具解析模型的结构,自动获取输入和输出属性。本文文末提供了 Netron 工具的使用说明,供您参考。
tiny-yolov3 模型的输入和输出属性说明如下:
输入(INPUTS)
参数 | 属性 | 释义 |
---|---|---|
input_1 | Tensor 形状: |
|
image_shape | Tensor 形状: |
|
输出(OUTPUTS)
参数 | 属性 | 释义 |
---|---|---|
yolonms_layer_1 | Tensor 形状: |
|
yolonms_layer_1:1 | Tensor 形状: |
|
yolonms_layer_1:2 | Tensor 形状: |
|
注意
本章节介绍了在边缘智能控制台创建一个自定义模型,并将该模型部署到边缘智能节点的方法。
登录边缘智能控制台。
在左侧导航栏顶部的 我的项目 区域,选择您的项目。
在左侧导航栏,选择 边缘推理 > 模型管理。
创建自定义模型。
为自定义模型创建一个版本。
发布版本。
部署模型服务。
说明
由于模型的批处理大小固定为 1,因此最大批处理大小必须设置为 0,表示不允许推理服务动态调整批处理大小。
等待模型服务部署成功。
当模型服务的状态变为 运行中,表示模型服务部署成功。这时,单击模型服务的名称,可以访问模型详情。在 基本信息 标签页,获取模型服务的 服务地址 和 模型ID。
本章节提供了一份调用模型服务的示例代码。您可以使用示例代码来验证模型服务是否正常。
下载示例代码压缩包。
获取示例代码压缩包后,对压缩包进行解压缩。
tar -xzvf poetry_tiny_yolov3_client.tar.gz cd yolov3_client/
解压缩后,您将获得示例代码文件。
安装示例代码。
说明
安装示例代码前,确保您使用的 Python 版本不低于 3.8。本教程所使用的 Python 版本是 3.9.6。
示例代码支持以下两种安装方式:
(推荐)通过requirements.txt
文件安装
pip3 install -r requirements.txt
通过包管理工具 poetry 安装
安装 poetry。
curl -sSL https://install.python-poetry.org | python3 -
安装依赖。
poetry install
安装完成后,您可以查看images
目录。该目录下有两张测试图片。
运行示例代码。
将下面命令中的--model_id
和 --server_url
的值替换为模型服务的 模型ID 和 服务地址。
poetry run python yolov3_client.py --model_id <模型ID> --server_url <服务地址 | ip:port> --image_dir ./images --label_file coco_classes.txt --result_dir ./result
查看推理结果。
在 result
目录下会看到图片输出。
输出的图片包含推理结果。
以下是本教程所使用的示例代码的详细说明。示例代码包含以下处理逻辑:
triton_client = httpclient.InferenceServerClient(url=args.server_url)
def letterbox_image(image, size): iw, ih = image.size w, h = size scale = min(w/iw, h/ih) nw = int(iw*scale) nh = int(ih*scale) image = image.resize((nw,nh), Image.BICUBIC) new_image = Image.new('RGB', size, (128,128,128)) new_image.paste(image, ((w-nw)//2, (h-nh)//2)) return new_image def preprocess(img): model_image_size = (416, 416) boxed_image = letterbox_image(img, tuple(reversed(model_image_size))) image_data = np.array(boxed_image, dtype='float32') image_data /= 255. image_data = np.transpose(image_data, [2, 0, 1]) return image_data def pre_process_image(images, image_dir, process=False): image_list = [] shape_list = [] for name in images: path = os.path.join(image_dir, name) image = Image.open(path) image_shape = np.array([image.size[1], image.size[0]], dtype=np.float32) image = preprocess(image) image_list.append(image.astype(np.float32)) shape_list.append(image_shape.astype(np.float32)) image = np.stack(image_list, axis=0) image_shape = np.stack(shape_list, axis=0) return image, image_shape
模型的前处理用到以下 3 个函数:
letterbox_image(image, size)
iw, ih
) 和目标尺寸 (w, h
)。nw, nh
) 不会超过目标尺寸。BICUBIC
) 来调整图像的尺寸。new_image
)。这个图像的尺寸是目标尺寸,背景色是 RGB 中的 (128,128,128)
。preprocess(img)
(416, 416)
。letterbox_image
函数将图像调整到模型的图像尺寸。float32
。np.transpose
函数来重排数组的维度,将颜色通道维度从最后一个维度移动到第一个维度,并返回预处理后的图像数据。images
: 一个包含图像文件名的列表。image_dir
: 图像文件的存储路径。process
: 一个布尔参数,用于决定是否对图像进行预处理。pre_process_image(images, image_dir, process=False)
image_list
和 shape_list
,用于存储预处理过的图像和它们的形状。images
列表中的每个图像文件名,将每个文件名与 image_dir
进行拼接,形成完整的文件路径。Image.open
函数打开图像文件,并获取图像的尺寸 image.size
。image.size
返回的是一个元组,形式为 (width, height)
。因此,image.size[1]
是图像的高度,image.size[0]
是图像的宽度。Image.open
函数将图像尺寸数组转换为 np.float32
类型,并存储在 image_shape
中。preprocess
函数,对图像进行预处理。np.float32
类型,并加入到 image_list
列表中。同样地,将 image_shape
加入到 shape_list
中。np.stack
函数,沿着第一个轴(axis=0)堆叠 image_list
和 shape_list
中的所有元素,形成一个新的 Numpy 数组 image
和 image_shape
。image
和 image_shape
。request_images, request_shapes = pre_process_image(images, image_dir) inputs = [] inputs.append(httpclient.InferInput('input_1', request_images.shape, "FP32")) inputs.append(httpclient.InferInput('image_shape', request_shapes.shape, "FP32")) inputs[0].set_data_from_numpy(request_images, binary_data=False) inputs[1].set_data_from_numpy(request_shapes, binary_data=False) outputs = [] outputs.append(httpclient.InferRequestedOutput('yolonms_layer_1:2', binary_data=False)) outputs.append(httpclient.InferRequestedOutput('yolonms_layer_1:1', binary_data=False)) outputs.append(httpclient.InferRequestedOutput('yolonms_layer_1', binary_data=False)) results = triton_client.infer(args.model_id, inputs=inputs, outputs=outputs)
def post_process_image(images, image_dir, results, result_dir, lables): indices = results.as_numpy("yolonms_layer_1:2") scores = results.as_numpy("yolonms_layer_1:1") boxes = results.as_numpy("yolonms_layer_1") out_boxes, out_scores, out_classes = [], [], [] for idx_ in indices[0]: out_classes.append(idx_[1]) out_scores.append(scores[tuple(idx_)]) idx_1 = (idx_[0], idx_[2]) out_boxes.append(boxes[idx_1]) for index,image_name in enumerate(images): image_path = os.path.join(image_dir, image_name) image = Image.open(image_path) img = ImageDraw.Draw(image) x_size = image.size[0] y_size = image.size[1] x_ratio = x_size / 416 y_ratio = y_size / 416 for idx, box in enumerate(out_boxes): score = out_scores[idx] shape = [box[1], box[0], box[3], box[2]] img.rectangle(shape, outline ="red") lable = lables[int(out_classes[idx])] show_txt = f"{lable}:{score:.2}" print(show_txt, shape) img.text((box[1] , box[0] ), show_txt, fill=(0,255,0,255), align ="left") result_path = os.path.join(result_dir, f"{image_name[:-4]}_infer.png") image.save(result_path)
后处理过程包含以下逻辑: