基于Notebook的图像分类模板使用指南
目录
1.创建并启动Notebook
2.训练图像分类-单图单标签模型
3.配置并发布模型
4.校验模型
5.部署在线服务
基于Notebook的图像分类模板使用指南
本文采用图像分类-单图单标签模板开发模型的过程为例,介绍从创建 Notebook 任务到引入数据、训练模型,再到保存模型、部署模型的全流程。
创建并启动Notebook
1、在 BML 左侧导航栏中点击『Notebook』
2、在 Notebook 页面点击『新建』,在弹出框中填写公司/个人信息以及项目信息,示例如下:
填写基础信息

填写项目信息

3、对 Notebook 任务操作入口中点击『配置』进行资源配置,示例如下:
选择开发语言、AI 框架,由于本次采用 PaddleClas 进行演示,所以需要选择 python3.7、PaddlePaddle2.0.0。选择资源规格,由于深度学习所需的训练资源一般较多,需要选择GPU V100的资源规格。

完成配置后点击『确认并启动』,即可启动 Notebook,启动过程中需要完成资源的申请以及实例创建,请耐心等待。
4、等待 Notebook 启动后,点击『打开』,页面跳转到 Notebook,即完成 Notebook 的创建与启动,示例如下:

训练图像分类-单图单标签模型
下载 PaddleClas 套件
打开进入 Notebook,点击进入终端,输入如下命令切换到 /home/work/ 目录。
1cd /home/work/
本文以 PaddleClas 代码库 release/2.3 分支为例,输入如下命令下载并解压代码包。整个过程需要数十秒,请耐心等待。
1wget https://github.com/PaddlePaddle/PaddleClas/archive/refs/heads/release/2.3.zip && unzip 2.3.zip
安装环境
在终端环境中,安装该版本的 PaddleClas 代码包依赖的 paddlepaddle-gpu,执行如下命令:
1python -m pip install paddlepaddle-gpu==2.1.3.post101 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html
安装完成后,使用 python 或 python3 进入python解释器,输入 import paddle ,再输入 paddle.utils.run_check()
如果出现 PaddlePaddle is installed successfully!,说明成功安装。
准备训练数据
训练数据是模型生产的重要条件,优质的数据集可以很大程度上的提升模型训练效果,准备数据可以参考链接。本文所用的安全帽检测数据集可前往此链接进行下载:下载链接。
1、导入用户数据。
在 Notebook 中并不能直接访问您在 BML 中创建的数据集,需要通过左边选择栏的导入数据集选项,进行数据集导入。导入的数据位于用户目录的 data/ 文件夹(当原始数据集有更新时,不会自动同步,需要手工进行同步)。

注:若在BML中未创建数据集,请先参考 数据服务 ,创建、上传、标注数据集。
2、数据转换。
PaddleClas 训练所需要的数据格式与 BML 默认的数据格式有所不同,所以需要利用脚本将导入的数据转为 PaddleClas 支持的数据格式,并进行3:7切分。
PaddleClas 默认支持的标注格式为 txt,文件中每行格式如下:
1图像相对路径 图像的label_id(数字类别)(注意:中间有空格)
转换脚本如下:
1import os
2import json
3import glob
4import codecs
5import random
6
7def parse_label_list(src_data_dir, save_dir):
8 """
9 遍历标注文件,获取label_list
10 :param src_data_dir:
11 :return:
12 """
13 label_list = []
14 anno_files = glob.glob(src_data_dir + "*.json")
15 for anno_f in anno_files:
16 annos = json.loads(codecs.open(anno_f).read())
17 for object in annos["labels"]:
18 label_list.append(object["name"])
19 label_list = list(set(label_list))
20 with codecs.open(os.path.join(save_dir, "label_list.txt"), 'w', encoding="utf-8") as f:
21 for id, label in enumerate(label_list):
22 f.writelines("%s:%s\n" % (id, label))
23 return len(label_list), label_list
24
25
26def trans_split_data(src_data_dir, save_dir):
27 """转换数据格式,并3/7分切分数据"""
28 image_list = glob.glob(src_data_dir + "*.[jJPpBb][PpNnMm]*")
29 image_label_list = []
30 for image_file in image_list:
31 json_file = image_file.split('.')[0]+".json"
32 if os.path.isfile(json_file):
33 annos = json.loads(codecs.open(json_file).read())
34 label = annos["labels"][0]["name"]
35 image_label_list.append("{} {}\n".format(os.path.basename(image_file), label_list.index(label)))
36
37 random.shuffle(image_label_list)
38 split_nums = int(len(image_label_list) * 0.3)
39 val_list = image_label_list[:split_nums]
40 train_list = image_label_list[split_nums:]
41 with open(os.path.join(save_dir, "train.txt"), 'w') as f:
42 f.writelines(train_list)
43 with open(os.path.join(save_dir, "val.txt"), 'w') as f:
44 f.writelines(val_list)
45
46class_nums, label_list = parse_label_list("/home/work/data/${dataset_id}/", "/home/work/PretrainedModel/")
47trans_split_data("/home/work/data/${dataset_id}/", "/home/work/PretrainedModel/")
将上述脚本存放为 coversion.py 代码脚本,并将脚本最后两行的 ${dataset_id} 替换为所指定数据集的 ID(下图红框中的ID),在终端中运行即可。

运行代码。
1python coversion.py
运行之后将在 PretrainedModel/ 文件夹下生成对应的数据文件,包括 label_list.txt、tran.txt、val.txt。

训练模型
1、在终端中打开 PaddleClas 目录。
1cd /path/to/PaddleClas
2、修改yaml配置文件。
本文以 ResNet50_vd 为例,配置文件路径为:
1/home/work/PaddleClas-release-2.3/ppcls/configs/ImageNet/ResNet/ResNet50_vd.yaml
1# global configs
2Global:
3 checkpoints: null
4 pretrained_model: null
5 output_dir: ./output/
6 # 使用GPU训练
7 device: gpu
8 # 每几个轮次保存一次
9 save_interval: 1
10 eval_during_train: True
11 # 每几个轮次验证一次
12 eval_interval: 1
13 # 训练轮次
14 epochs: 10
15 print_batch_step: 10
16 use_visualdl: False #是否开启可视化
17 # used for static mode and model export
18 # 图像大小
19 image_shape: [3, 224, 224]
20 save_inference_dir: ./inference
21
22# model architecture
23Arch:
24 # 采用的网络
25 name: ResNet50_vd
26 # 类别数
27 class_num: 1000
28
29# loss function config for traing/eval process
30Loss:
31 Train:
32 - CELoss:
33 weight: 1.0
34 epsilon: 0.1
35 Eval:
36 - CELoss:
37 weight: 1.0
38
39
40Optimizer:
41 name: Momentum
42 momentum: 0.9
43 lr:
44 name: Cosine
45 learning_rate: 0.1
46 regularizer:
47 name: 'L2'
48 coeff: 0.00007
49
50
51# data loader for train and eval
52DataLoader:
53 Train:
54 dataset:
55 name: ImageNetDataset
56 # 数据集根路径
57 image_root: /home/work/data/${dataset_id}
58 # 前面生产得到的训练集列表文件路径
59 cls_label_path: /home/work/PretrainedModel/train_list.txt
60 # 数据预处理
61 transform_ops:
62 - DecodeImage:
63 to_rgb: True
64 channel_first: False
65 - RandCropImage:
66 size: 224
67 - RandFlipImage:
68 flip_code: 1
69 - NormalizeImage:
70 scale: 1.0/255.0
71 mean: [0.485, 0.456, 0.406]
72 std: [0.229, 0.224, 0.225]
73 order: ''
74 batch_transform_ops:
75 - MixupOperator:
76 alpha: 0.2
77
78 sampler:
79 name: DistributedBatchSampler
80 batch_size: 64
81 drop_last: False
82 shuffle: True
83 loader:
84 num_workers: 0
85 use_shared_memory: True
86
87 Eval:
88 dataset:
89 name: ImageNetDataset
90 # 数据集根路径
91 image_root: /home/work/data/${dataset_id}
92 # 前面生产得到的训练集列表文件路径
93 cls_label_path: /home/work/PretrainedModel/val_list.txt
94 transform_ops:
95 - DecodeImage:
96 to_rgb: True
97 channel_first: False
98 - ResizeImage:
99 resize_short: 256
100 - CropImage:
101 size: 224
102 - NormalizeImage:
103 scale: 1.0/255.0
104 mean: [0.485, 0.456, 0.406]
105 std: [0.229, 0.224, 0.225]
106 order: ''
107 sampler:
108 name: DistributedBatchSampler
109 batch_size: 64
110 drop_last: False
111 shuffle: False
112 loader:
113 num_workers: 0
114 use_shared_memory: True
115
116Infer:
117 infer_imgs: docs/images/whl/demo.jpg
118 batch_size: 10
119 transforms:
120 - DecodeImage:
121 to_rgb: True
122 channel_first: False
123 - ResizeImage:
124 resize_short: 256
125 - CropImage:
126 size: 224
127 - NormalizeImage:
128 scale: 1.0/255.0
129 mean: [0.485, 0.456, 0.406]
130 std: [0.229, 0.224, 0.225]
131 order: ''
132 - ToCHWImage:
133 PostProcess:
134 # 输出的可能性最高的前topk个
135 name: Topk
136 topk: 5
137 # 前面得到标签文件
138 class_id_map_file: /home/work/PretrainedModel/label_list.txt
139
140Metric:
141 Train:
142 Eval:
143 - TopkAcc:
144 topk: [1, 5]
根据相关文件的地址对上述yaml文件进行修订,主要修改点:类别数、训练和验证集的路径、标签文件地址、训练和验证的 num_workers 需修改为 0。
注:Notebook 因为是单卡的,需要将 num_workers 改为0,在本地的话则需要根据实际情况进行更改
1修改类别数
220行:class_num: 5
3
4修改训练集的路径(数据集id根据您自己的情况调整)
549行:image_root: /home/work/data/302273
650行:cls_label_path: /home/work/PretrainedModel/train.txt
7
8修改训练GPU
974行:num_workers: 0
10
11修改验证集的路径(数据集id根据您自己的情况调整)
1280行:image_root: /home/work/data/302273
1381行:cls_label_path: /home/work/PretrainedModel/val.txt
14
15修改验证GPU
16101行:num_workers: 0
17
18修改标签文件地址
19124行:class_id_map_file: /home/work/PretrainedModel/label_list.txt
3、训练模型。
在终端中执行以下命令,开始模型训练。
1cd PaddleClas-release-2.3/
2python tools/train.py -c ./ppcls/configs/ImageNet/ResNet/ResNet50_vd.yaml
4、模型预测。
在终端中执行以下命令,开始模型预测。
1python tools/infer.py \
2 -c ./ppcls/configs/ImageNet/ResNet/ResNet50_vd.yaml \
3 -o Infer.infer_imgs=/home/work/data/${dataset_id}/xxx.jpg \
4 -o Global.pretrained_model=output/ResNet50_vd/best_model
预测结果如下:

5、导出模型。
在终端中执行以下命令,将最佳模型转为可以用于发布的 inference 模型。
1python tools/export_model.py \
2 -c ./ppcls/configs/ImageNet/ResNet/ResNet50_vd.yaml \
3 -o Global.pretrained_model=output/ResNet50_vd/best_model \
4 -o Global.save_inference_dir=/home/work/PretrainedModel/
在终端中执行以下命令,将导出模型重命名为以“model”为前缀的模型文件。
1mv /home/work/PretrainedModel/inference.pdiparams /home/work/PretrainedModel/model.pdiparams
2mv /home/work/PretrainedModel/inference.pdmodel /home/work/PretrainedModel/model.pdmodel
3mv /home/work/PretrainedModel/inference.pdiparams.info /home/work/PretrainedModel/model.pdiparams.info
分类模型部署时默认配置如下,在 /home/work/PretrainedModel/ 路径下创建并保存 infer_cfg.yaml文件。若需自定义相关参数,可在此基础上进行修改。
1Global:
2 inference_model_dir: "/home/work/PretrainedModel/"
3 batch_size: 1
4 use_gpu: True
5 enable_mkldnn: True
6 cpu_num_threads: 10
7 enable_benchmark: True
8 use_fp16: False
9 ir_optim: True
10 use_tensorrt: False
11 gpu_mem: 8000
12 enable_profile: False
13PreProcess:
14 transform_ops:
15 - ResizeImage:
16 resize_short: 256
17 - CropImage:
18 size: 224
19 - NormalizeImage:
20 scale: 0.00392157
21 mean: [0.485, 0.456, 0.406]
22 std: [0.229, 0.224, 0.225]
23 order: ''
24 channel_num: 3
25 - ToCHWImage:
在此步骤后/home/work/PretrainedModel目录下将有如下文件:

- 数据文件:label_list.txt、train.txt、val.txt
- 模型文件:model.pdiparams、model.pdmodel、model.pdiparams.info
- 配置文件:infer_cfg.yaml
6、生成模型版本。
Notebook中的模型文件只有生成模型版本后,才可以执行发布和部署功能:
- 请确保要保存的模型文件在
/home/work/PretrainedModel目录下。模型支持版本管理功能,在保存时可以生成新版本也可以覆盖已有的且尚未部署的模型版本,每个版本的模型都可以独立部署。每个模型版本中保存的模型文件大小上限为1.5GB。 - 在保存模式时也可以将训练模型的代码一并保存。代码支持版本管理功能,用户再次启动Notebook时,可以使用指定的代码版本来初始化Notebook工作空间即/home/work目录下data以外的空间。每个代码版本中保存的文件大小上限为150M。
点击左侧导航栏中的生成模型版本组件,打开弹窗填写信息。

模型属性-选择 AI 框架选择 PaddlePaddle2.0.0,若上一次操作中进行了代码保存,可在“代码版本”选择对应的代码版本。

选择模型文件-选择 label_list.txt、model.pdiparams、model.pdmodel 文件,若有自定义 infer_cfg.yaml 文件,也一并选上(非必须)。

点击『生成』即可生成模型版本,生成模型版本一般需要数十秒,请耐心等待。
配置并发布模型
BML NoteBook 的图像分类单标签模板产出的模型支持进行部署,下面以 PaddleClas 的模型为例,详细介绍如何配置模型:
1、查看前置条件是否满足:需要训练完成,并生成了相应的模型生成版本(详见训练模型的第六步)。
2、回到 BML Notebook 列表页,点击『模型发布列表』即可进入配置页面。

3、点击配置,即可进入配置流程。

4、填写模型信息。

5、选择待发布的模型文件,点击确定按钮。

其中:
- 网络结构文件
model.pdmodel:必需选择,且名字固定。 - 网络参数文件
model.pdiparams:必需选择,且名字固定。 - 模型标签文件:
label_list.txt,非必须,主要看自定义逻辑代码是如何实现的。本文中在【配置出入参及数据处理逻辑脚本】的脚本代码里面会用到,所以需要选择。 - 预/后处理配置文件等其他文件:
infer_cfg.yaml,非必须,主要看自定义逻辑代码是如何实现的。本文中在【配置出入参及数据处理逻辑脚本】的脚本代码里面会用到,所以需要选择。
6、配置出入参及数据逻辑处理。
这部分配置主要实现自定义的模型预处理和后处理逻辑。该脚本用于将选择的模型文件发布成模型服务。用户可以通过修改 PredictWrapper 的 preprocess 和 postprocess 方法来实现自定义的请求预处理和预测结果后处理。当提交该脚本时,系统会根据用户选择的模型文件和脚本内容,来验证是否可以启动模型服务,只有验证通过,才可以进行模型效果校验以及将模型发布到模型仓库。
实现脚本有一些建议和限制:
CustomException必需存在且是异常类;在自定义的逻辑中,建议当处理进入错误的分支时,抛出CustomException并指定message,指定的message在请求回包中会作为error_msg返回。PredictWrapper类必需存在,且必需包含preprocess和postprocess两个方法。PredictWrapper的preprocess和postprocess方法,是用户自定义模型服务请求预处理和预测结果后处理的入口。preprocess方法接收的第一个参数为用户请求的json字典,对于图像类服务,传入图像的参数key必须是"image",且传入的是图片的 base64 编码。-
系统会根据
postprocess方法的返回结果result类型的不同,做以下处理:Plain Text1 - dict: 不作修改 2 - list: 转换为 {"results": result}- 其他: 转换为 {"result": result}
- 最终的处理结果可以转换为json字符串,以确保能够正常将结果作为请求回包返回
- 单击“提交”,完成模型配置。 提交后,系统会自动对当前版本模型进行代码验证,通过后模型会更新为“有效”状态。
配置出入参及数据处理逻辑脚本:实现图片的预处理和模型输出结果后处理的逻辑;
这一步是比较关键,但也比较复杂的一步。上面介绍了脚本实现时的限制和建议。这里针对PaddleClass套件,实现了一套对应的脚本代码(在后面),可以点击立即编辑,将脚本代码拷贝替换掉。

注:
1 - 可以看下平台预置代码文件,以及各个类及函数的注释了解实现细节。这里贴了`PaddleClass套件`对应的脚本文件,整个代码比较长,但大部分内容都拷贝于[PaddleClas套件的推理示例](https://github.com/PaddlePaddle/PaddleClas/blob/develop/deploy/python/predict_cls.py)。如果自身训练模型比较特殊,当前脚本支持不了,需要自己去PaddleClas套件中寻找逻辑,并更新到该脚本中。
2 - 对于处理脚本里面的预处理`preprocess`方法,第一个返回参数`input_info`为字典类型,其中字典的key 为模型的输入节点名称,需要根据模型修改。
3 
4 如果不知道训练模型的输入节点名称是什么,可以先用下面提供的脚本进行一次模型验证,验证日志里面查看对应的输入名称。
5 
6 
PaddleClas脚本样例:
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# *******************************************************************************
4#
5# Copyright (c) 2020 Baidu.com, Inc. All Rights Reserved
6#
7# *******************************************************************************
8
9# 注意事项:
10# 该脚本用于将通过notebook训练产出的模型发布成模型服务
11# 用户可以通过修改PredictWrapper的preprocess和postprocess方法来实现自定义的请求预处理和预测结果后处理
12# 当在EasyDL提交该脚本时,系统会根据用户选择的模型文件和脚本内容,来验证是否可以启动模型服务,如果验证通过,即可进行模型效果校验以及部署模型服务
13# 下面是修改脚本的一些限制和建议:
14# 1. CustomException必须存在且是异常类;在自定义的逻辑中,建议当处理进入错误的分支时,抛出CustomException并指定message,指定的message在请求回包中会作为error_msg返回;
15# 2. PredictWrapper类必须存在,且必需包含preprocess和postprocess两个方法;
16# 3. PredictWrapper的preprocess和postprocess方法,是用户自定义模型服务请求预处理和预测结果后处理的入口;
17# 4. preprocess方法接收的第一个参数为用户请求的json字典,对于图像类服务,传入图像的参数key必须是"image",且传入的是图片的base64编码
18# 5. 系统会根据postprocess方法的返回结果`result`类型的不同,做以下处理:
19# dict: 不作修改
20# list: 转换为 {"results": result}
21# 其他: 转换为 {"result": result}
22# 处理结果将转换为json字符串,以确保系统能正常将结果作为请求回包返回。
23
24import base64
25import math
26import random
27
28import cv2
29import logging
30import numpy as np
31import six
32import yaml
33
34from functools import partial
35from PIL import Image
36
37DEFAULT_TOP_NUM = 6
38
39
40class CustomException(RuntimeError):
41 """
42 进行模型验证和部署服务必需的异常类,缺少该类在代码验证时将会失败
43 在处理异常数据或者请求时,推荐在`PredictWrapper`中的自定义预处理preprocess和后处理postprocess函数中抛出`CustomException`类,
44 并为`message`指定准确可读的错误信息,以便在服务响应包中的`error_msg`参数中返回。
45 """
46 def __init__(self, message, orig_error=None):
47 """ 根据`message`初始化 """
48 super(CustomException, self).__init__(message)
49 self.orig_error = orig_error
50
51
52class PredictWrapper(object):
53 """ 模型服务预测封装类,支持用户自定义对服务请求数据的预处理和模型预测结果的后处理函数 """
54
55 def __init__(self, model_path):
56 """
57 根据`model_path`初始化`PredictWrapper`类,如解析label_list.txt,加载模型输出标签id和标签名称的映射关系
58 :param model_path: 该目录下存放了用户选择的模型版本中包含的所有文件
59 """
60 # 加载推理配置文件,获取【预处理配置】及【标签id和名称的映射关系】
61 ops_config = [
62 {"ResizeImage": {"resize_short": 256}},
63 {"CropImage": {"size": 224}},
64 {"NormalizeImage": {"scale": 0.00392157, "mean": [0.485, 0.456, 0.406],
65 "std": [0.229, 0.224, 0.225], "order": '', "channel_num": 3}},
66 {"ToCHWImage": {}},
67 ]
68 conf_path = '{model_path}/{conf_file}'.format(model_path=model_path, conf_file='infer_cfg.yml')
69 try:
70 with open(conf_path) as conf_fin:
71 infer_conf = yaml.load(conf_fin, Loader=yaml.FullLoader)
72 if "PreProcess" in infer_conf and "transform_ops" in infer_conf["PreProcess"]:
73 ops_config = infer_conf["PreProcess"]["transform_ops"]
74 except:
75 pass
76
77 self._preprocess_ops = create_operators(ops_config)
78
79 try:
80 label_path = '{model_path}/{label_file}'.format(model_path=model_path, label_file='label_list.txt')
81 self._label_list = {}
82 with open(label_path) as label_list_f:
83 for index, line in enumerate(label_list_f):
84 line = line.strip()
85 label_info = line.split(":", 1)
86 if len(label_info) == 2:
87 self._label_list[int(label_info[0])] = label_info[1]
88 else:
89 self._label_list[index] = line
90 except:
91 pass
92
93 def preprocess(self, request_body, **preprocess_args):
94 """
95 自定义对请求体的预处理,针对图像类模型服务,包括对图片对图像的解析、转化等
96 :param request_body: 请求体的json字典
97 :param preprocess_args: 从`{model_path}/preprocess_args.json`中加载的预处理参数字典,json文件不存在时,传入为空字典
98 :return:
99 data: 用于模型预测的输入。注意:data结构为dict,key为模型输入节点的名称,value为对应需要喂入的值,batch只能为1
100 infer_args: 用于模型预测的其他参数
101 request_context: 透传给自定义后处理函数`postprocess`的参数,例如指定返回预测结果的top N,过滤低score的阈值threshold.
102 """
103 # decode image from base64 string in request
104 try:
105 image_b64 = request_body['image']
106 img_bin = base64.b64decode(image_b64)
107 except KeyError:
108 raise CustomException('Missing required parameter')
109 except Exception:
110 raise CustomException('Invalid BASE64')
111
112 data = np.frombuffer(img_bin, dtype='uint8')
113 im = cv2.imdecode(data, 1) # BGR mode, but need RGB mode
114 im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
115
116 # paddle cls
117 # code: https://github.com/PaddlePaddle/PaddleClas/blob/develop/deploy/python/predict_cls.py
118 try:
119 for preprocess_op in self._preprocess_ops:
120 im = preprocess_op(im)
121
122 input_info = {"x": im[np.newaxis, ...]}
123 except Exception:
124 raise CustomException('Failed decoding input')
125
126 return input_info, {}, {"top_num": request_body.get("top_num", DEFAULT_TOP_NUM)}
127
128 def postprocess(self, infer_result, request_context, **postprocess_args):
129 """
130 自定义对图像分类模型输出结果的后处理,例如根据score对label进行排序,获取top N分类结果等
131 :param infer_result: fluid模型的预测结果
132 :param request_context: 自定义预处理函数中返回的`request context`
133 :param postprocess_args: 从`{model_path}/postprocess_args.json`中加载的后处理参数字典,json文件不存在时,传入为空字典
134 :return: request results 请求的处理结果
135 """
136 top_num = request_context["top_num"]
137 result = infer_result[0]
138 result = get_result_list(result)
139
140 indices = np.flip(np.argsort(result), 0)[:top_num]
141 top_results = list()
142 for item in indices:
143 item = int(item)
144 top_results.append({
145 'score': float(result[item]),
146 'name': self._label_list.get(item, str(item))
147 })
148 return top_results
149
150
151def create_operators(params):
152 """
153 create operators based on the config
154 Args:
155 params(list): a dict list, used to create some operators
156 """
157 assert isinstance(params, list), ('operator config should be a list')
158 op_mapping = {
159 "UnifiedResize": UnifiedResize,
160 "DecodeImage": DecodeImage,
161 "ResizeImage": ResizeImage,
162 "CropImage": CropImage,
163 "RandCropImage": RandCropImage,
164 "RandFlipImage": RandFlipImage,
165 "NormalizeImage": NormalizeImage,
166 "ToCHWImage": ToCHWImage,
167 }
168 ops = []
169 for operator in params:
170 assert isinstance(operator,
171 dict) and len(operator) == 1, "yaml format error"
172 op_name = list(operator)[0]
173 param = {} if operator[op_name] is None else operator[op_name]
174 op = op_mapping[op_name](**param)
175 ops.append(op)
176
177 return ops
178
179
180class UnifiedResize(object):
181 def __init__(self, interpolation=None, backend="cv2"):
182 _cv2_interp_from_str = {
183 'nearest': cv2.INTER_NEAREST,
184 'bilinear': cv2.INTER_LINEAR,
185 'area': cv2.INTER_AREA,
186 'bicubic': cv2.INTER_CUBIC,
187 'lanczos': cv2.INTER_LANCZOS4
188 }
189 _pil_interp_from_str = {
190 'nearest': Image.NEAREST,
191 'bilinear': Image.BILINEAR,
192 'bicubic': Image.BICUBIC,
193 'box': Image.BOX,
194 'lanczos': Image.LANCZOS,
195 'hamming': Image.HAMMING
196 }
197
198 def _pil_resize(src, size, resample):
199 pil_img = Image.fromarray(src)
200 pil_img = pil_img.resize(size, resample)
201 return np.asarray(pil_img)
202
203 if backend.lower() == "cv2":
204 if isinstance(interpolation, str):
205 interpolation = _cv2_interp_from_str[interpolation.lower()]
206 # compatible with opencv < version 4.4.0
207 elif interpolation is None:
208 interpolation = cv2.INTER_LINEAR
209 self.resize_func = partial(cv2.resize, interpolation=interpolation)
210 elif backend.lower() == "pil":
211 if isinstance(interpolation, str):
212 interpolation = _pil_interp_from_str[interpolation.lower()]
213 self.resize_func = partial(_pil_resize, resample=interpolation)
214 else:
215 logging.warning(
216 "The backend of Resize only support \"cv2\" or \"PIL\". \"%s\" is unavailable. Use \"cv2\" instead.",
217 backend
218 )
219 self.resize_func = cv2.resize
220
221 def __call__(self, src, size):
222 return self.resize_func(src, size)
223
224
225class OperatorParamError(ValueError):
226 """ OperatorParamError
227 """
228 pass
229
230
231class DecodeImage(object):
232 """ decode image """
233
234 def __init__(self, to_rgb=True, to_np=False, channel_first=False):
235 self.to_rgb = to_rgb
236 self.to_np = to_np # to numpy
237 self.channel_first = channel_first # only enabled when to_np is True
238
239 def __call__(self, img):
240 if six.PY2:
241 assert type(img) is str and len(
242 img) > 0, "invalid input 'img' in DecodeImage"
243 else:
244 assert type(img) is bytes and len(
245 img) > 0, "invalid input 'img' in DecodeImage"
246 data = np.frombuffer(img, dtype='uint8')
247 img = cv2.imdecode(data, 1)
248 if self.to_rgb:
249 assert img.shape[2] == 3, 'invalid shape of image[%s]' % (
250 img.shape)
251 img = img[:, :, ::-1]
252
253 if self.channel_first:
254 img = img.transpose((2, 0, 1))
255
256 return img
257
258
259class ResizeImage(object):
260 """ resize image """
261
262 def __init__(self,
263 size=None,
264 resize_short=None,
265 interpolation=None,
266 backend="cv2"):
267 if resize_short is not None and resize_short > 0:
268 self.resize_short = resize_short
269 self.w = None
270 self.h = None
271 elif size is not None:
272 self.resize_short = None
273 self.w = size if type(size) is int else size[0]
274 self.h = size if type(size) is int else size[1]
275 else:
276 raise OperatorParamError("invalid params for ReisizeImage for '\
277 'both 'size' and 'resize_short' are None")
278
279 self._resize_func = UnifiedResize(
280 interpolation=interpolation, backend=backend)
281
282 def __call__(self, img):
283 img_h, img_w = img.shape[:2]
284 if self.resize_short is not None:
285 percent = float(self.resize_short) / min(img_w, img_h)
286 w = int(round(img_w * percent))
287 h = int(round(img_h * percent))
288 else:
289 w = self.w
290 h = self.h
291 return self._resize_func(img, (w, h))
292
293
294class CropImage(object):
295 """ crop image """
296
297 def __init__(self, size):
298 if type(size) is int:
299 self.size = (size, size)
300 else:
301 self.size = size # (h, w)
302
303 def __call__(self, img):
304 w, h = self.size
305 img_h, img_w = img.shape[:2]
306
307 if img_h < h or img_w < w:
308 raise Exception(
309 "The size({h}, {w}) of CropImage must be greater than size({img_h}, {img_w}) of image. "
310 "Please check image original size and size of ResizeImage if used.".format(h=h, w=w, img_w=img_w,
311 img_h=img_h)
312 )
313
314 w_start = (img_w - w) // 2
315 h_start = (img_h - h) // 2
316
317 w_end = w_start + w
318 h_end = h_start + h
319 return img[h_start:h_end, w_start:w_end, :]
320
321
322class RandCropImage(object):
323 """ random crop image """
324
325 def __init__(self,
326 size,
327 scale=None,
328 ratio=None,
329 interpolation=None,
330 backend="cv2"):
331 if type(size) is int:
332 self.size = (size, size) # (h, w)
333 else:
334 self.size = size
335
336 self.scale = [0.08, 1.0] if scale is None else scale
337 self.ratio = [3. / 4., 4. / 3.] if ratio is None else ratio
338
339 self._resize_func = UnifiedResize(
340 interpolation=interpolation, backend=backend)
341
342 def __call__(self, img):
343 size = self.size
344 scale = self.scale
345 ratio = self.ratio
346
347 aspect_ratio = math.sqrt(random.uniform(*ratio))
348 w = 1. * aspect_ratio
349 h = 1. / aspect_ratio
350
351 img_h, img_w = img.shape[:2]
352
353 bound = min((float(img_w) / img_h) / (w**2),
354 (float(img_h) / img_w) / (h**2))
355 scale_max = min(scale[1], bound)
356 scale_min = min(scale[0], bound)
357
358 target_area = img_w * img_h * random.uniform(scale_min, scale_max)
359 target_size = math.sqrt(target_area)
360 w = int(target_size * w)
361 h = int(target_size * h)
362
363 i = random.randint(0, img_w - w)
364 j = random.randint(0, img_h - h)
365
366 img = img[j:j + h, i:i + w, :]
367
368 return self._resize_func(img, size)
369
370
371class RandFlipImage(object):
372 """ random flip image
373 flip_code:
374 1: Flipped Horizontally
375 0: Flipped Vertically
376 -1: Flipped Horizontally & Vertically
377 """
378
379 def __init__(self, flip_code=1):
380 assert flip_code in [-1, 0, 1
381 ], "flip_code should be a value in [-1, 0, 1]"
382 self.flip_code = flip_code
383
384 def __call__(self, img):
385 if random.randint(0, 1) == 1:
386 return cv2.flip(img, self.flip_code)
387 else:
388 return img
389
390
391class NormalizeImage(object):
392 """ normalize image such as substract mean, divide std
393 """
394
395 def __init__(self,
396 scale=None,
397 mean=None,
398 std=None,
399 order='chw',
400 output_fp16=False,
401 channel_num=3):
402 if isinstance(scale, str):
403 scale = eval(scale)
404 assert channel_num in [
405 3, 4
406 ], "channel number of input image should be set to 3 or 4."
407 self.channel_num = channel_num
408 self.output_dtype = 'float16' if output_fp16 else 'float32'
409 self.scale = np.float32(scale if scale is not None else 1.0 / 255.0)
410 self.order = order
411 mean = mean if mean is not None else [0.485, 0.456, 0.406]
412 std = std if std is not None else [0.229, 0.224, 0.225]
413
414 shape = (3, 1, 1) if self.order == 'chw' else (1, 1, 3)
415 self.mean = np.array(mean).reshape(shape).astype('float32')
416 self.std = np.array(std).reshape(shape).astype('float32')
417
418 def __call__(self, img):
419 from PIL import Image
420 if isinstance(img, Image.Image):
421 img = np.array(img)
422
423 assert isinstance(img,
424 np.ndarray), "invalid input 'img' in NormalizeImage"
425
426 img = (img.astype('float32') * self.scale - self.mean) / self.std
427
428 if self.channel_num == 4:
429 img_h = img.shape[1] if self.order == 'chw' else img.shape[0]
430 img_w = img.shape[2] if self.order == 'chw' else img.shape[1]
431 pad_zeros = np.zeros(
432 (1, img_h, img_w)) if self.order == 'chw' else np.zeros(
433 (img_h, img_w, 1))
434 img = (np.concatenate(
435 (img, pad_zeros), axis=0)
436 if self.order == 'chw' else np.concatenate(
437 (img, pad_zeros), axis=2))
438 return img.astype(self.output_dtype)
439
440
441class ToCHWImage(object):
442 """ convert hwc image to chw image
443 """
444
445 def __init__(self):
446 pass
447
448 def __call__(self, img):
449 from PIL import Image
450 if isinstance(img, Image.Image):
451 img = np.array(img)
452
453 return img.transpose((2, 0, 1))
454
455
456def get_result_list(results):
457 """ 判断模型输出结果的shape,并返回一维的soft-maxed 数组"""
458 shape = np.array(results).shape
459 max_val = max(shape)
460 max_dim = shape.index(max_val)
461 ret = results
462 for i in range(max_dim):
463 ret = ret[0]
464 result_arr = []
465 for item in ret:
466 real_item = item
467 for i in range(max_dim + 1, len(shape)):
468 real_item = real_item[0]
469 result_arr.append(real_item)
470 return np.array(result_arr)
7、点击提交即可进入模型验证阶段,验证时间一般需要数十秒,请耐心等待。

验证通过后,显示有效。

8、点击发布,填写相关信息后,即可发布成功。
9、点击左侧导航栏模型管理,即可查看发布成功的模型。

校验模型
1、点击『版本列表』。

2、点击『校验模型』。

3、点击『启动模型校验』,启动约需5分钟,请耐心等待。

4、上传图像即可开始校验,示例如下:

部署在线服务
1、点击『版本列表』。

2、点击部署-在线服务。

3、完成信息填写及资源规格选择后,即可开始部署。

4、部署过程需要数十秒时间,请耐心等待。部署完成后,示例如下:

5、API调用方法请参考 公有云部署管理。
