不期速成 日拱一卒 不期速成 日拱一卒
首页
技术
测试
分类
标签
归档
关于
首页
技术
测试
分类
标签
归档
关于
  • 测试样本

  • 测试框架

    • AITLibra智能算法效果测试框架
      • 1. 前言
      • 2. 设计理念
      • 3. 使用指导
        • 3.1 环境准备
        • 3.2 安装
        • 3.3 命令概览
        • 3.4 教学示例
        • 3.5. 配置文件解读
        • 3.6. 插件代码解读
    • AITLibraManager埋点服务设计
  • AITLibra-智能算法效果测试提效降本的实践
  • NLU(自然语言理解)智能算法测试从1到N的改进
  • 智能算法测试平台基础设施
  • AIT
  • 测试框架
2021-05-11

AITLibra智能算法效果测试框架

# 1. 前言

智能化算法测试团队的核心工作内容之一是算法效果测试,区别于业务功能测试“对与错”的验证,算法效果测试更偏向于“好与坏”的验证,为使得“好与坏”的评判结果更具备可信度,通常需要分析大量的被测样本数据的测试结果,因此,核心工作投入是对算法模型的输入、输出数据的统计、比对、分析、评判的测试代码开发。

在算法测试前期探索过程中,各类算法并行立项,首先需要解决的是各类算法模型测试结果可信度、可靠行的问题。截至2020年中,对目前我司4大类 12小类的算法测试,我们从测试数据设计、评估指标选择、测试方法应用等角度,逐一明确并落地了算法效果测试执行方案,有效保障测试结果的可靠性、可信度。

但事物发展到一定阶段,逐步成熟,其自由开放的多样性必然带来高昂的重复成本,没有统一标准必然带来管理的混乱与低效,若想更进一步的发展,必然走向统一与标准化,以史为鉴,从春秋战国各自为政到大秦帝国的统一,书同文,车同轨的出现是历史的选择,也是一个强大帝国的必然选择,唯有统一标准才能降低成本,提高效率,带来生产力的发展。

智能化算法测试的当前主要工作是算法效果测试(未来更重要的应该是算法效果在业务价值的评估),其核心工作量是算法效果代码的开发。

因此我们在2020年下半年,分析查看了各类算法服务的效果测试代码,看到了百花齐放,看到了八仙过海各显神通,但其中有大量的重复,为了完成同一个目的得不同实现方式,各种不同的实现方式和处理方式,有些代码应当更模块化,逻辑应当更条理……种种问题给我们的算法效果测试的继续发展带来了一定的困难:

  • 不能形成团队规模的智慧积累,仅有个人层面的少量代码积累;
  • 开发效率低,代码评审成本高,代码交接困难,维护成本更高;
  • 团队的算法效果测试代码开发仅仅少数人能够胜任。

基于逐渐发现上述问题,我们同步明确需要解决的重点问题,制定了算法测试下一阶段的发展目标-——实现智能算法效果测试代码设计开发的统一和标准化,解决重复投入、等待浪费的问题,我们需要一个能解决80%算法效果测试场景的通用智能算法效果测试框架,因此进行了一系列的布局和铺垫:

  1. 组织和人员能力的建设:
  • 【2020年08月起】成立技术评审专家组,对所有算法效果测试代码开展代码评审,发现问题,改正问题,在审查与被审查中提高人员能力,提取算法效果测试代码的设计与开发标准;
  • 【2020年11月起】进行了一次Python基础能力的摸底考试,让团队成员对自己的开发能力有一个客观认识;
  • 【2020年11月起】在团队内部开展Python基础的知识分享,并辅以结合日常工作需求的Python编程练习;
  • 【2020年11月起】测试架构师深入一线开展算法效果测试代码的设计、开发,参与到项目中,将个人想法理念落地到生产实践中检验;
  • 【2021年03月起】提供算法效果测试代码项目模板,以“填空题”的形式试点让更多同学加入算法效果测试代码的设计和开发,验证代码项目模板的理论和操作可行性。
  1. 基础设施和工具的建设:
  • 【2021年3月起】AITUtils (opens new window) 智能算法测试工具库,封装算法效果测试常用的代码为Python包,并提供API文档便于检索查阅。
  • 【2021年6月起】AITDBManager (opens new window) + AITDBClient (opens new window) 智能算法测试数据管家,对测试样本数据进行中心化、标准化管理;

在团队的共同努力下,一件件事在有序落地和推进,有了这些基础,我们的算法效果测试框架就具备诞生的土壤了。

通过AITLibra智能算法效果测试框架,我们约束了基本的开发、设计的规范、提供了便利的开发脚手架。

更重要的是,我们将测试开发技术在算法测试领域的应用、拓展、创新的实践机会交给了每一位同学,鼓励每一位同学在测试技术方向能够学以致用、用以促学、学用相长,不断促进我们产品质量保障综合能力的提升——-共创、共建、共荣。

# 2. 设计理念

通过观察分析与实践检验,我司80%的智能算法效果测试代码逻辑都能套入如下5个核心流程中:

image-20210511150252249

根据上述核心流程,我们设计了AITLibra框架,整体设计思路如下:

智能化算法自动化-AITLibra

通过这种对核心环节的抽象,我们可以将通用“业务”逻辑进行封装,由AITLibra框架提供,对于常用的基础方法,由AITUtils (opens new window)提供支持,双剑合璧,可以有效降低绝大多数算法效果自动化的代码量为传统开发模式的80%以下。

以人头检测算法效果自动化为例进行代码量化统计

使用代码模板的方式进行开发,需要yaml配置文件71行,测试工程师所面对的代码行数如下:

image-20210517105003791

使用AITLibra框架仅需开发,需要yaml配置文件194行,测试工程师所面对的代码行数如下:

image-20210517095526508

AITLibra将代码行数降低为之前的15%(204 / 1353),减少了1149行,大大降低了测试工程师的心智负担,虽然配置文件行数有所增长,从71行增加到了160行,但配置文件中有大量相似配置,因此并不算复杂,我们会在下文详细介绍。

# 3. 使用指导

# 3.1 环境准备

修改命令行终端的窗口宽度,避免后续框架运行时,因窗口宽度不足导致进度条刷屏。

image-20210513155938686

安装Python3.7.7,创建Python虚拟环境

若没有virtualenv命名,请先安装pip install -U virtualenv

image-20210511161416123

# 3.2 安装

配置PypiManager (opens new window),然后安装AITLibra

pip install -U AITLibra
1

检查是否安装成功

image-20210519160143448

# 3.3 命令概览

image-20210511161732954

# 3.4 教学示例

# 3.4.1. 创建教学示例项目

使用libra tutorial生成教学示例项目,并进入项目目录

image-20210519160320011

# 3.4.2. 执行项目测试

# 3.4.2.1 运行项目以获取算法服务的预测结果数据

查看配置文件libra.yaml工作流开关,确认工作流配置如下

pipeline:
  backup: true
  build_sample: true
  fast_api_response: true
  analyze: true
  statistic: true
  report: true
1
2
3
4
5
6
7

然后执行libra run命令,等待运行结束

image-20210519161909693

# 3.4.3 查看测试结果报表

测试结果报表在report_data目录下

image-20210519162030328

# 3.5. 配置文件解读

查看生成的教学示例配置文件libra.yaml,分为以下几个部分,下面我们逐个部分解读。

image-20210511161950988

# 3.5.1. config

# 配置信息
config:
  # 测试任务信息
  task:
    operator: 专业团队
    description: 第一轮效果测试
  # 项目信息
  project:
    name: head_detect
    description: 人头识别算法服务
    version: '3.2'
    api_url: http://172.17.46.1:9005/multiMedia/computing/v1/headDetect
  # 目录配置,默认值,请勿修改
  sample_dir: sample_data
  sample_attachment_dir: attachment
  api_data_dir: api_data
  analysis_data_dir: analysis_data
  statistic_data_dir: statistic_data
  report_data_dir: report_data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

此部分配置为全局的一些基础配置,包含了我们对本次测试任务的描述,相关的项目信息,以及相关初始化目录的名称,需要用户关注的内容为task、project节点下的配置,需要注意version的值加引号。

# 3.5.2. pipeline

# 各流程的运行开关, true代表执行此步骤,false代表不执行此步骤
pipeline:
  backup: true
  build_sample: true
  fast_api_response: true
  analyze: false
  statistic: false
  report: false
1
2
3
4
5
6
7
8

backup - 备份文件夹和文件,通常修改测试样本、更新被测服务后,会用到此功能

build_sample - 预处理标注数据,构建测试样本数据集

fast_api_response - 并发请求被测智能算法服务API,获取所有测试样本的预测结果数据

analyze - 研判每一条测试样本的预测数据与标注数据的结果

statistic - 统计与计算评估指标

report - 输出测试结果报表

需要注意的是,我们进行算法效果测试代码开发调试的过程中一般仅选取少量的测试数据,整个开发调试活动主要分为4个阶段,我们要按需控制开关:

  • 从标注数据构造测试样本,获取算法服务对测试样本的预测结果数据,此阶段仅打开build_sample、fast_api_response两个开关即可;
  • 根据预测结果数据与测试样本的标注数据进行比对分析、研判预测结果是否正确,此阶段仅打开analyze开关即可;
  • 完成比对分析研判的逻辑后,我们就得到了分析结果数据,要开始指标统计逻辑的设计了,此阶段仅打开statistic开关即可;
  • 最后,根据以及生成的分析结果、统计结果,我们要输出整体的测试结果报表,此阶段仅打开report开关即可;

完成整体的开发调试后,我们可以放入全量的测试数据,打开全部开关:

  • 将backup、build_sample、fast_api_response、analyze、statistic、report都设置为true

# 3.5.3. backup

# 备份文件或目录到备份文件夹中,支持配置正则表达式,默认在项目根目录下搜索,此处的值均需要使用单引号
backup:
  # 整个文件夹
  dir: ['temp']
  # 某个文件
  file: ['*_response.jl', 'analysis_result.jl']
1
2
3
4
5
6

按需备份文件夹和文件,若没有需要备份的数据,将两个配置的值设置为空array即可

# 3.5.4. build_sample

# 构建测试样本数据
build_sample:
  # 存储测试样本标注数据的json line文件,默认值,请勿修改
  annotation_sample_json_line: $sample_dir/annotation_sample.jl
  attachment_dir: $sample_attachment_dir
  # func方法会生成annotation_sample_json_line文件,并将测试数据的附件存储到attachment_dir目录
  func: $sample_builder()
1
2
3
4
5
6
7

根据需求对原始标注数据进行预处理,构建测试样本数据集。

对于测试数据文件对象在服务器存储需要下载的场景,按照模块划分的标准,我们应当在这一步骤,完成文件的下载。

此配置文件中有个func节点,从现在开始,我们接触到了AITLibra框架的拓展功能了---- 代码插件。

代码插件存放在在libra_plugin.py文件中。

此处配置的代码插件为$sample_builder(),即此部分操作会执行libra_plugin.py文件中的 sample_builder函数。

对于插件代码函数的解释,会在后文详细说明。

# 3.5.5. fast_api_response

# 请求被测API,获取响应数据序列化到json line文件
fast_api_response:
  # API请求并发数
  test_thread_number: 3
  # 请求超时设置
  time_out: 10
  http_method: post
  # 请求体的类型
  payload_type: json
  payload_func: $payload_func()
  response_json_schema: $response_json_schema
  # 默认值,请勿修改
  api_response:
    success_json_line: $api_data_dir/success_json_response.jl
    fail_json_line: $api_data_dir/fail_json_response.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

配置并发请求被测服务的参数,用户主要关心的配置项为test_thread_number、http_method、payload_type。

payload_func 和response_json_schema为libra_plugin.py文件中定义的代码插件。

payload_func为读取annotation_sample_json_line数据的每一条son标注数据,构造请求体数据结构的插件代码;

response_json_schema为对响应体数据进行成功或失败研判的jsonschema配置。

# 3.5.6. analyze

analyze:
  extractor:
    # 使用jmespath提取json数据, jmespath资料和工具: http://172.16.114.1:8666/
    annotated_rule: "faces[*].{head_completion: head_completion, head_occlusion: head_occlusion, head_angle: head_angle, box: box}"
    predicted_rule: "api_response.headObjectRows[*].{rect: rect}"
  func: $analyzer(annotated_data, predicted_data)
  # 分析结果输出到此文件中,用于后续的统计环节使用,默认值,请勿修改
  analysis_json_line: $analysis_data_dir/analysis_result.jl
1
2
3
4
5
6
7
8

算法服务预测结果数据与测试样本标注数据比对分析模块。

extractor为数据提取器,提供两种数据的jmespath提取规则,提取success_json_response.jl文件中每一行数据。

  • annotated_rule为每一条用于分析研判的标注数据的提取规则;
  • predicted_rule为每一条算法服务返回的预测结果数据提取规则。

举个例子:

success_json_response.jl文件的其中一行示例数据如下

{
	"faces": [
		{
			"head_completion": "yes_head_completion",
			"head_occlusion": "existhead_occlusion",
			"box_size": [
				62,
				50
			],
			"head_angle": "front",
			"box": [
				972,
				140,
				1022,
				140,
				1022,
				202,
				972,
				202
			],
			"boxtype": "head_box",
			"key_point": []
		},
		{
			"head_completion": "yes_head_completion",
			"head_occlusion": "existhead_occlusion",
			"box_size": [
				56,
				44
			],
			"head_angle": "front",
			"box": [
				842,
				140,
				886,
				140,
				886,
				196,
				842,
				196
			],
			"boxtype": "head_box",
			"key_point": [],
			"undefined": ""
		}
	],
	"imageId": "c383a039-4d93-4f75-868e-af7bfd20bfff",
	"url": "http://172.16.42.66:8060/result/picturedownload?path=13a9eff9-ad08-412a-8f8e-baddde96a7a2/19169/10.1.130.24_01_20200324172936632.jpeg",
	"api_response": {
		"noid": "111",
		"statusCode": "1200",
		"msg": "MULTIMEDIA_SUCCESS",
		"costTime": 124,
		"headObjectRows": [
			{
				"imageID": "c383a039-4d93-4f75-868e-af7bfd20bfff",
				"rect": {
					"x1": 851,
					"y1": 162,
					"x2": 896,
					"y2": 214
				},
				"qualityType": "0",
				"feature": null,
				"qualityScore": 0
			},
			{
				"imageID": "c383a039-4d93-4f75-868e-af7bfd20bfff",
				"rect": {
					"x1": 976,
					"y1": 146,
					"x2": 1019,
					"y2": 197
				},
				"qualityType": "0",
				"feature": null,
				"qualityScore": 0
			}
		]
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

使用 http://172.16.114.1:8666/ 工具进行jmespath规则的调试

使用annotated_rule规则: faces[*].{head_completion: head_completion, head_occlusion: head_occlusion, head_angle: head_angle, box: box}提取数据,可以获取到如下数据:

[
  {
    "head_completion": "yes_head_completion",
    "head_occlusion": "existhead_occlusion",
    "head_angle": "front",
    "box": [
      972,
      140,
      1022,
      140,
      1022,
      202,
      972,
      202
    ]
  },
  {
    "head_completion": "yes_head_completion",
    "head_occlusion": "existhead_occlusion",
    "head_angle": "front",
    "box": [
      842,
      140,
      886,
      140,
      886,
      196,
      842,
      196
    ]
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

使用predicted_rule规则api_response.headObjectRows[*].{rect: rect}提取数据,可以获取到如下数据:

[
  {
    "rect": {
      "x1": 851,
      "y1": 162,
      "x2": 896,
      "y2": 214
    }
  },
  {
    "rect": {
      "x1": 976,
      "y1": 146,
      "x2": 1019,
      "y2": 197
    }
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

根据上述的数据,编写插件代码:$analyzer(annotated_data, predicted_data),其中annotated_data参数即为annotated_rule规则提取的数据,predicted_data参数即为predicted_rule规则提取的数据,我们编写插件代码,实现分析比对逻辑,判定预测结果是否正确,构造分析结果数据。

分析结果的数据结构如下:

{
	"expect_list": [
		{
			"head_completion": "yes_head_completion",
			"head_occlusion": "existhead_occlusion",
			"head_angle": "front",
			"box": [
				972,
				140,
				1022,
				140,
				1022,
				202,
				972,
				202
			]
		},
		{
			"head_completion": "yes_head_completion",
			"head_occlusion": "existhead_occlusion",
			"head_angle": "front",
			"box": [
				842,
				140,
				886,
				140,
				886,
				196,
				842,
				196
			]
		}
	],
	"recognize_list": [
		{
			"rect": {
				"x1": 851,
				"y1": 162,
				"x2": 896,
				"y2": 214
			}
		},
		{
			"rect": {
				"x1": 976,
				"y1": 146,
				"x2": 1019,
				"y2": 197
			}
		}
	],
	"correct_list": [
		{
			"head_completion": "yes_head_completion",
			"head_occlusion": "existhead_occlusion",
			"head_angle": "front",
			"box": [
				972,
				140,
				1022,
				140,
				1022,
				202,
				972,
				202
			]
		}
	],
	"error_list": [
		{
			"rect": {
				"x1": 851,
				"y1": 162,
				"x2": 896,
				"y2": 214
			}
		}
	],
	"loss_list": [
		{
			"head_completion": "yes_head_completion",
			"head_occlusion": "existhead_occlusion",
			"head_angle": "front",
			"box": [
				842,
				140,
				886,
				140,
				886,
				196,
				842,
				196
			]
		}
	],
	"one_to_more_list": [],
	"expect_count": 2,
	"recognize_count": 2,
	"correct_count": 1,
	"error_count": 1,
	"loss_count": 1,
	"status": "不通过"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

# 3.5.7. statistic

statistic:
  extractor:
    # 使用jmespath提取analy_result.jl文件的每一条json数据
    expect_count: "analysis_result.expect_count"
    recognize_count: "analysis_result.recognize_count"
    correct_count: "analysis_result.correct_count"
    error_count: "analysis_result.error_count"
    loss_count: "analysis_result.loss_count"
    status: "analysis_result.status"
  unit:
    # 计算因子单元
    # 框架内置函数,
    # $sum,对extractor提取的每一条数据进行累加,处理对象是int类型
    # $length,对extractor提取的每一条数据对象的长度进行累加,处理对象是array或object类型
    # $group,对extractor提取的每一条数据进行分类计数统计,处理对象为string类型
    # $total_count,计算总共有多少条测试样本
    total_sample_count: $total_count()
    total_recognize_count: $sum($recognize_count)
    total_except_count: $sum($expect_count)
    # IOU >= 0.5即为正确
    total_correct_count: $sum($correct_count)
    # IOU < 0.5即为错误
    total_error_count: $sum($error_count)
    # 识别出目标即为FN
    total_loss_count: $sum($loss_count)
    # 对每张图片的结果进行分类计数
    total_status_count: $group($status)
  formula:
    # judge_rule: >、>=、<、<=、=
    # 人头准确率
    head_accurate_rate:
      expression:  $total_correct_count / $total_recognize_count if $total_correct_count != 0 and $total_recognize_count != 0 else 0.0
      threshold_value: 0.75
      judge_rule: ">="
    # 人头检测召回率
    head_recall_rate:
      expression:  $total_correct_count / $total_except_count if $total_correct_count != 0 else 0.0
      threshold_value: 0.75
      judge_rule: ">="
    # 人头检测精确率
    head_precision_rate:
      expression:  $total_correct_count / $total_recognize_count if $total_correct_count != 0 else 0.0
      threshold_value: 0.75
      judge_rule: ">="
    # F1精确率-召回率权衡
    f1_score:
      expression: (2 * $head_precision_rate * $head_recall_rate) / ($head_precision_rate + $head_recall_rate)
      threshold_value: 0.5
      judge_rule: ">="
  # 通过宏定义模板生成各分析维度的配置文件
  loop:
    # 各分析维度统计
    - item:
        # 人头是否完整
        head_completion: [yes, no]
        # 人头是否有遮挡
        head_occlusion: [exist, no]
        # 人头角度
        head_angle: [lateral, front, back, top]
      extractor:
        expect_<%value%>_<%key%>_array: "analysis_result.expect_list[?<%key%>=='<%value%>_<%key%>']"
        correct_<%value%>_<%key%>_array: "analysis_result.correct_list[?<%key%>=='<%value%>_<%key%>']"
      # 各维度分析统计因子
      unit:
        total_expect_<%value%>_<%key%>_count: $length(expect_<%value%>_<%key%>_array)
        total_correct_<%value%>_<%key%>_count: $length(correct_<%value%>_<%key%>_array)
      # 各维度指标计算
      formula:
        <%value%>_<%key%>_recall_rate:
          expression:  $total_correct_<%value%>_<%key%>_count / $total_expect_<%value%>_<%key%>_count if $total_correct_<%value%>_<%key%>_count != 0 else 0.0
          threshold_value: 0.75
          judge_rule: ">="
    # 默认值,请勿修改
  statistic_json: $statistic_data_dir/statistic.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

指标计算模块,分为三个核心部分extractor、unit,formula,其中还有一个可选的高级功能loop。

# 3.5.7.1 extractor

数据提取模块,使用``jmespath语法规则从analysis_result.jl`文件的每一行数据中提取用于指标计算的数据赋值给规则对应的变量,与上文示例相同,此处不在赘述,举几个例子演示下作用:

analysis_result.jl文件的其中一行数据内容示例如下(省略其他信息,仅展示此处用到的analyze_result节点的数据:

{
	"faces": [],
	"imageId": "c383a039-4d93-4f75-868e-af7bfd20bfff",
	"url": "http://172.16.42.66:8060/result/picturedownload?path=13a9eff9-ad08-412a-8f8e-baddde96a7a2/19169/10.1.130.24_01_20200324172936632.jpeg",
	"api_response": {},
	"analysis_result": {
		"expect_list": [
			{
				"head_completion": "yes_head_completion",
				"head_occlusion": "existhead_occlusion",
				"head_angle": "front",
				"box": [
					972,
					140,
					1022,
					140,
					1022,
					202,
					972,
					202
				]
			},
			{
				"head_completion": "yes_head_completion",
				"head_occlusion": "existhead_occlusion",
				"head_angle": "front",
				"box": [
					842,
					140,
					886,
					140,
					886,
					196,
					842,
					196
				]
			}
		],
		"recognize_list": [
			{
				"rect": {
					"x1": 851,
					"y1": 162,
					"x2": 896,
					"y2": 214
				}
			},
			{
				"rect": {
					"x1": 976,
					"y1": 146,
					"x2": 1019,
					"y2": 197
				}
			}
		],
		"correct_list": [
			{
				"head_completion": "yes_head_completion",
				"head_occlusion": "existhead_occlusion",
				"head_angle": "front",
				"box": [
					972,
					140,
					1022,
					140,
					1022,
					202,
					972,
					202
				]
			}
		],
		"error_list": [
			{
				"rect": {
					"x1": 851,
					"y1": 162,
					"x2": 896,
					"y2": 214
				}
			}
		],
		"loss_list": [
			{
				"head_completion": "yes_head_completion",
				"head_occlusion": "existhead_occlusion",
				"head_angle": "front",
				"box": [
					842,
					140,
					886,
					140,
					886,
					196,
					842,
					196
				]
			}
		],
		"one_to_more_list": [],
		"expect_count": 2,
		"recognize_count": 2,
		"correct_count": 1,
		"error_count": 1,
		"loss_count": 1,
		"status": "不通过"
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 3.5.7.1.1 例1

expect_count: "analysis_result.expect_count" 会从数据中根据jmespath规则提取出对应的数据:2,将2赋值给except_count变量,待后续模块使用。

# 3.5.7.1.2 例2

expect_yes_head_completion_array: "analysis_result.expect_list[?head_completion=='yes_head_completion']"会从数据中根据jmespath规则提取出对应的数据:

[
  {
    "head_completion": "yes_head_completion",
    "head_occlusion": "existhead_occlusion",
    "head_angle": "front",
    "box": [
      972,
      140,
      1022,
      140,
      1022,
      202,
      972,
      202
    ]
  },
  {
    "head_completion": "yes_head_completion",
    "head_occlusion": "existhead_occlusion",
    "head_angle": "front",
    "box": [
      842,
      140,
      886,
      140,
      886,
      196,
      842,
      196
    ]
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

将此数据赋值给expect_yes_head_completion_array变量,待后续模块使用。

# 3.5.7.1.3 例3

correct_head_angle_top_array: "analysis_result.correct_list[?head_angle=='top']",数据中没有符合此jmespath的数据,则在本条数据中,此配置的correct_head_angle_top_array变量对应的值为[],一个空数组。

# 3.5.7.2 unit

计算因子单元模块,使用AITLibra框架内置的函数对extracor模块提取的数据进行简单的计算处理。

此处目前支持3个内置函数,sum、length、group

  • sum函数:对extractor提取的每一条数据进行累加,处理对象是int类型;
  • length函数:对extractor提取的每一条数据对象的长度进行累加,处理对象是array或object类型;
  • group函数:对extractor提取的每一条数据进行分类计数统计,处理对象为string类型

分别举个例子讲解下:

# 3.5.7.2.1 sum函数示例

total_except_count: $sum($expect_count),此配置的意思是对analysis_result.jl文件的每一条数据中的expect_count变量值进行累加计算。

比如jl文件共5条数据,根据extractor模块对expect_count变量的定义,共提取出5个数据,分别为[2, 3, 2, 1, 2],total_except_count: $sum($expect_count)就等价于2+3+2+1+2进行求和运算,total_except_count变量的值为的10。

# 3.5.7.2.2 length函数示例

total_expect_yes_head_completion_count: $length(expect_yes_head_completion_array),此配置的意思是对analysis_result.jl文件的每一条数据中的expect_yes_head_completion_array变量的长度进行累加,将累加结果赋值给total_expect_yes_head_completion_count变量。

# 3.5.7.2.3 group函数示例

total_status_count: $group($status),此配置的意思是对analysis_result.jl文件的每一条数据中的status变量进行分组统计数量,等同于sql语句的group by效果。

比如jl文件共有3条数据,每条数据的status变量值对应为["通过", "不通过", "通过"]

则total_status_count: $group($status)就等价于将{"通过": 2, "不通过": 1}赋值给total_status_count。

# 3.5.7.2.4 total_count函数示例

total_sample_count: $total_count(),此配置的意思是堆analysis_result.jl文件中一共有多少行测试样本数据进行数据总量统计,等同于sql语种中的count(*)效果。

比如jl文件中共有10条数据,则total_sample_count: $total_count()就等价于将10赋值给total_sample_count。

# 3.5.7.3 formula

指标公式定义和结果判定模块。

此模块主要配置内容如下:

# 指标名称
head_accurate_rate:
  # 指标计算表达式
  expression:  $total_correct_count / $total_recognize_count if $total_recognize_count != 0 else 0.0
  # 指标通过的阈值
  threshold_value: 0.75
  # 指标通过的判定逻辑
  judge_rule: ">="
1
2
3
4
5
6
7
8

其中指标名称用户可以自定义,请按照Python的变量命名规范进行命名,命名请做到见闻知意,比如xxx_recall_rate,xxx_accurate_rate,xxx_precision_rate;

其后的三个参数expression、threshold_value、judge_rule是不能修改,必须包含的。

  • expression:使用unit或formula节点中已经定义的变量,按照Python语法编写指标的计算公式;
  • threshold_value:通常为浮点数,指标期望阈值
  • judge_rule:即为实际指标与期望指标的比较关系,结果为True则指标通过,支持的判定逻辑为:>、>=,=、<、<=,range

注意,此处有个高级用法,当judge_rule的值为range时,threshold_value可以填写为数学区间,注意区间用双引号包裹,举个例子:

# 指标名称
head_accurate_rate:
  # 指标计算表达式
  expression:  $total_correct_count / $total_recognize_count if $total_recognize_count != 0 else 0.0
  # 指标通过的阈值
  threshold_value: "[0.75, 1.0]"
  # 指标通过的判定逻辑
  judge_rule: "range"
1
2
3
4
5
6
7
8
# 3.5.7.4 loop

此模块为高级功能,可选配,若没有此类需求,可以不写此节点,默认生成的配置文件没有此节点信息

此模块是为了解决算法各分析维度分析内容高度相似,比如:人头的朝向分为前、后、左、右,其分析配置项高度相似,逐个编写配置文件存在冗余且效率低,因此设计了宏定义模板来自动生成这些结构高度相似的配置文件。

此模块的配置示例如下:

# 通过宏定义模板生成各分析维度的配置文件
loop:
  # 各分析维度统计
  - item:
      # 人头是否完整
      head_completion: [yes, no]
      # 人头是否有遮挡
      head_occlusion: [exist, no]
      # 人头角度
      head_angle: [lateral, front, back, top]
    extractor:
      expect_<%value%>_<%key%>_array: "analysis_result.expect_list[?<%key%>=='<%value%>_<%key%>']"
      correct_<%value%>_<%key%>_array: "analysis_result.correct_list[?<%key%>=='<%value%>_<%key%>']"
    # 各维度分析统计因子
    unit:
      total_expect_<%value%>_<%key%>_count: $length(expect_<%value%>_<%key%>_array)
      total_correct_<%value%>_<%key%>_count: $length(correct_<%value%>_<%key%>_array)
    # 各维度指标计算
    formula:
      <%value%>_<%key%>_recall_rate:
        expression:  $total_correct_<%value%>_<%key%>_count / $total_expect_<%value%>_<%key%>_count if $total_correct_<%value%>_<%key%>_count != 0 else 0.0
        threshold_value: 0.75
        judge_rule: ">="
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

主要包含item、extractor、unit、formula四个子模块。

# 3.5.7.4.1 item

注意,item前面有个短横符号,这代表,此级缩进的内容是个是个array,我们可以写多个item分组。

我们着重说明下item的含义,通过定义此模块,可以将各个维度进行预先变量声明,其为一个键值对结构,键为要分析的维度,数组型的值为分析的维度内容。

比如人头完整度(head_completion)有两个维度:人头完整(yes)和人头不完整(no),其表示即如下:

# 各分析维度统计
- item:
    # 人头是否完整
    head_completion: [yes, no]
1
2
3
4
# 3.5.7.4.2 在extractor、unit、formula模板中使用变量

对于item中定义的变量,我们使用模板占位符来引用,并自动根据变量键值对组合进行循环渲染,使用<%value%>渲染键值对数组中的每个值,使用<%key%>渲染键值对的键。

举个例子:

对于7.4.1小节的item配置示例,我们使用如下宏定义模板配置

# 通过宏定义模板生成各分析维度的配置文件
loop:
  # 各分析维度统计
  - item:
      # 人头是否完整
      head_completion: [yes, no]
    extractor:
      expect_<%value%>_<%key%>_array: "analysis_result.expect_list[?<%key%>=='<%value%>_<%key%>']"
      correct_<%value%>_<%key%>_array: "analysis_result.correct_list[?<%key%>=='<%value%>_<%key%>']"
    # 各维度分析统计因子
    unit:
      total_expect_<%value%>_<%key%>_count: $length(expect_<%value%>_<%key%>_array)
      total_correct_<%value%>_<%key%>_count: $length(correct_<%value%>_<%key%>_array)
    # 各维度指标计算
    formula:
      <%value%>_<%key%>_recall_rate:
        expression:  $total_correct_<%value%>_<%key%>_count / $total_expect_<%value%>_<%key%>_count if $total_correct_<%value%>_<%key%>_count != 0 else 0.0
        threshold_value: 0.75
        judge_rule: ">="
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

通过框架渲染将会生成运行时配置runtime_libra.yaml,其内容如下:

statistic:
  extractor:
    # 人头是否完整
    expect_yes_head_completion_array: "analysis_result.expect_list[?head_completion=='yes_head_completion']"
    correct_yes_head_completion_array : "analysis_result.correct_list[?head_completion=='yes_head_completion']"
    expect_no_head_completion_array: "analysis_result.expect_list[?head_completion=='no_head_completion']"
    correct_no_head_completion_array: "analysis_result.correct_list[?head_completion=='no_head_completion']"
  unit:
    # 计算因子单元
    # 各维度分析统计因子
    total_expect_yes_head_completion_count: $length(expect_yes_head_completion_array)
    total_correct_yes_head_completion_count: $length(correct_yes_head_completion_array)
    total_expect_no_head_completion_count: $length(expect_no_head_completion_array)
    total_correct_no_head_completion_count: $length(correct_no_head_completion_array)
  formula:
    yes_head_completion_recall_rate:
      expression:  $total_correct_yes_head_completion_count / $total_expect_yes_head_completion_count if $total_correct_yes_head_completion_count != 0 else 0.0
      threshold_value: 0.75
      judge_rule: ">="
    no_head_completion_recall_rate:
      expression:  $total_correct_no_head_completion_count / $total_expect_no_head_completion_count if $total_correct_no_head_completion_count != 0 else 0.0
      threshold_value: 0.75
      judge_rule: ">="
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

需要说明的是,loop宏定义生成的配置会在运行时自动嵌入到statistic配置的节点下,自动合并更新配置文件,可以查看runtime_libra.yaml来分析其生成的配置是否正确。

# 3.5.7.5 statistic.json结果示例

根据上述配置的定义,框架会计算出如下结果数据,以备后续报表模块使用

{
	"unit_data": {
		"total_recognize_count": 4898,
		"total_except_count": 4751,
		"total_correct_count": 4194,
		"total_error_count": 704,
		"total_loss_count": 557,
		"total_status_count": {
			"通过": 1184,
			"不通过": 601
		},
		"total_expect_yes_head_completion_count": 4665,
		"total_correct_yes_head_completion_count": 4126,
		"total_expect_no_head_completion_count": 86,
		"total_correct_no_head_completion_count": 68,
		"total_expect_existhead_occlusion_count": 1805,
		"total_correct_existhead_occlusion_count": 1522,
		"total_expect_no_head_occlusion_count": 2946,
		"total_correct_no_head_occlusion_count": 2672,
		"total_expect_head_angle_lateral_count": 1206,
		"total_correct_head_angle_lateral_count": 1088,
		"total_expect_head_angle_front_count": 2459,
		"total_correct_head_angle_front_count": 2176,
		"total_expect_head_angle_back_count": 969,
		"total_correct_head_angle_back_count": 824,
		"total_expect_head_angle_top_count": 117,
		"total_correct_head_angle_top_count": 106,
		"head_accurate_rate": 0.856267864434463,
		"head_recall_rate": 0.8827615238897074,
		"head_precision_rate": 0.856267864434463,
		"f1_score": 0.8693128821639546,
		"yes_head_completion_recall_rate": 0.8844587352625938,
		"no_head_completion_recall_rate": 0.7906976744186046,
		"existhead_occlusion_recall_rate": 0.843213296398892,
		"no_head_occlusion_recall_rate": 0.9069925322471147,
		"head_angle_lateral_recall_rate": 0.9021558872305141,
		"head_angle_front_recall_rate": 0.8849125660837739,
		"head_angle_back_recall_rate": 0.8503611971104231,
		"head_angle_top_recall_rate": 0.905982905982906
	},
	"statistic_data": {
		"head_accurate_rate": {
			"expression": "$total_correct_count / $total_recognize_count if $total_recognize_count != 0 else 0.0",
			"result": {
				"expression_value": 0.856267864434463,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"head_recall_rate": {
			"expression": "$total_correct_count / $total_except_count if $total_except_count != 0 else 0.0",
			"result": {
				"expression_value": 0.8827615238897074,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"head_precision_rate": {
			"expression": "$total_correct_count / $total_recognize_count if $total_recognize_count != 0 else 0.0",
			"result": {
				"expression_value": 0.856267864434463,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"f1_score": {
			"expression": "(2 * $head_precision_rate * $head_recall_rate) / ($head_precision_rate + $head_recall_rate)",
			"result": {
				"expression_value": 0.8693128821639546,
				"threshold_value": 0.5,
				"status": "通过"
			}
		},
		"yes_head_completion_recall_rate": {
			"expression": "$total_correct_yes_head_completion_count / $total_expect_yes_head_completion_count if $total_expect_yes_head_completion_count != 0 else 0.0",
			"result": {
				"expression_value": 0.8844587352625938,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"no_head_completion_recall_rate": {
			"expression": "$total_correct_no_head_completion_count / $total_expect_no_head_completion_count if $total_expect_no_head_completion_count != 0 else 0.0",
			"result": {
				"expression_value": 0.7906976744186046,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"existhead_occlusion_recall_rate": {
			"expression": "$total_correct_existhead_occlusion_count / $total_expect_existhead_occlusion_count if $total_expect_existhead_occlusion_count != 0 else 0.0",
			"result": {
				"expression_value": 0.843213296398892,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"no_head_occlusion_recall_rate": {
			"expression": "$total_correct_no_head_occlusion_count / $total_expect_no_head_occlusion_count if $total_expect_no_head_occlusion_count != 0 else 0.0",
			"result": {
				"expression_value": 0.9069925322471147,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"head_angle_lateral_recall_rate": {
			"expression": "$total_correct_head_angle_lateral_count / $total_expect_head_angle_lateral_count if $total_expect_head_angle_lateral_count != 0 else 0.0",
			"result": {
				"expression_value": 0.9021558872305141,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"head_angle_front_recall_rate": {
			"expression": "$total_correct_head_angle_front_count / $total_expect_head_angle_front_count if $total_expect_head_angle_front_count != 0 else 0.0",
			"result": {
				"expression_value": 0.8849125660837739,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"head_angle_back_recall_rate": {
			"expression": "$total_correct_head_angle_back_count / $total_expect_head_angle_back_count if $total_expect_head_angle_back_count != 0 else 0.0",
			"result": {
				"expression_value": 0.8503611971104231,
				"threshold_value": 0.75,
				"status": "通过"
			}
		},
		"head_angle_top_recall_rate": {
			"expression": "$total_correct_head_angle_top_count / $total_expect_head_angle_top_count if $total_expect_head_angle_top_count != 0 else 0.0",
			"result": {
				"expression_value": 0.905982905982906,
				"threshold_value": 0.75,
				"status": "通过"
			}
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139

# 3.5.8. report

report:
- sheet_name: 指标概览
  field_name_list: ["指标名称", "实际指标值", "期望指标值", "指标状态(是否通过)", "人头识别丢失数", "人头识别错误数", "人头识别正确数", "识别出来人头数", "期望识别出来的人头数"]
  data_list: $summary_statistic_data_list()
- sheet_name: 分析详情汇总
  field_name_list: $detail_field_name_list()
  data_list: $detail_data_list()
- sheet_name: 各分析维度统计
  field_name_list: ["分析维度", "指标名称", "实际指标值", "期望指标值", "指标状态(是否通过)", "识别正确人头数", "期望人头数"]
  data_list: $each_dim_statistic_data_list()
1
2
3
4
5
6
7
8
9
10

测试报表生成模块。

每个表格使用-进行分隔,每个表格包含三个配置项sheet_name、field_name_list、data_list。

  • sheet_name:字符串类型值,代表此表格写入excel文件的哪个sheet页;
  • field_name_list:数组类型值,可以直接定义数组,也可以使用插件代码函数生成数组;
  • data_list:嵌套数组类型值,必须使用插件代码定义的函数生成;

# 3.6. 插件代码解读

由框架生成的插件代码文件libra_plugin.py原始内容如下:

image-20210513140725437

# 3.6.1. 框架自动生成代码

# 3.6.1.1 自动import
import pathlib
from typing import List, Dict, Text

from AITLibra.builtins.load_process_data import load_analyze_data, load_statistic_data
1
2
3
4

框架会自动import一些内置方法,以及标准库的方法对象等。

# 3.6.1.2 自动声明及初始化全局变量

可以看到代码中有一部分与目录和数据相关变量:

# ============== 此部分代码由AITLibra框架自动生成,请勿修改 ===================
PROJECT_BASE_DIR_PATH = pathlib.Path(r'F:\AITLibraShow\head_detect')
API_DATA_DIR_PATH = PROJECT_BASE_DIR_PATH.joinpath(r'api_data')
SAMPLE_DATA_DIR_PATH = PROJECT_BASE_DIR_PATH.joinpath(r'sample_data')
ATTACHMENT_DATA_DIR_PATH = SAMPLE_DATA_DIR_PATH.joinpath(r'attachment')
ANALYSIS_DATA_DIR_PATH = PROJECT_BASE_DIR_PATH.joinpath(r'analysis_data')
STATISTIC_DATA_DIR_PATH = PROJECT_BASE_DIR_PATH.joinpath(r'statistic_data')
REPORT_DATA_DIR_PATH = PROJECT_BASE_DIR_PATH.joinpath(r'report_data')

# 读取analysis_result.jl文件
analyze_data = load_analyze_data(analysis_data_path=str(ANALYSIS_DATA_DIR_ABS_PATH),
                                 analyze_file='analysis_result.jl')

# 读取statistic.json文件
statistic_data = load_statistic_data(statistic_data_path=str(STATISTIC_DATA_DIR_ABS_PATH),
                                     statistic_file='statistic.json')

# ============== 此部分代码由AITLibra框架自动生成,请勿修改 ===================

# API响应体json校验规则,jsonschema规则请参阅 http://172.16.114.1:8832/2021/04/jsonschema常用关键字语法手册
response_json_schema: Dict = {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

框架会自动声明相关目录的路径pathlib对象,便于用户后续编写定制化方法时直接使用,这部分代码请勿修改。

框架还会自动完成分析结果和统计结果数据的加载,便于用户编写定制化方法时直接使用。

# 3.6.2 用户自定义方法

用户自定义方法的开发过程中,常用的一些方法我么已经封装到了AITUtils (opens new window)中,并且还在不断的丰富,请同学们务必优先使用AITUtils库中的方法,这可大大提高我们的开发效率,降低开发成本!

# 3.6.2.1 sample_builder插件方法

此插件用于libra.yaml配置文件的build_sample配置模块,由用户自定义设计开发,用于完成测试集json line文件的构建,请注意,此方法只需要构造出每一条测试样本的json数据结构即可,使用yield关键字将此方法变为一个生成器。

示例代码如下:

def sample_builder():
    """
    构建每一条测试样本标注数据的生成器
    :return:
    """
    one_annotation_data_dict: Dict
    # ===== 请完成构建一条标注测试样本数据的逻辑 ======
    # 查找原始标注数据txt文本
    original_data_file_path = search_in_path(root_path=SAMPLE_DATA_DIR_PATH, goal_glob='人头导出数据.txt')
    if not original_data_file_path:
        logger.error('未发现标注数据原始文件')
        sys.exit(-1)
    # 从原始标注数据中找到每一个人脸的标注数据
    original_image_label_data = load_json_line(original_data_file_path[0])
    for line in original_image_label_data:
        multi_image_annotation_data = line['images']
        for one_annotation_data_dict in multi_image_annotation_data:
            # 下载每张图片到附件目录
            image_url = one_annotation_data_dict.get('url')
            try:
                __download_image_to_attachment(image_url=image_url)
            except Exception as err:
                logger.error(f'下载图片失败: {err}')
                sys.exit(-1)
            yield one_annotation_data_dict
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

此方法要读取SAMPLE_DATA_DIR_PATH目录下预置的标注数据人头导出数据.txt,我们要将此数据内的结构转为一张图片作为一个测试样本,同时将远程服务器的图片下载到本地,因为获取文件实体也是测试样本集构建的步骤。

**这里我们要注意,根据模块化代码设计的理念,不应把测试样本文件的下载放到请求发送环节,**因为获取远程服务器的测试文件实体可能会失败,这种失败与被测服务毫无关系,若在发送请求获取响应环节进行这种错误将会影响整体测试的效率(毕竟我们测试API服务,性能通常较差,整个算法效果测试执行最耗时的就是发送请求获取响应!)。

上述代码中的__download_image_to_attachment方法为用户自定义的内部方法,方法示例如下:

def __download_image_to_attachment(image_url):
    """
    下载图片到附件目录
    """
    __temp_parse_data = urlparse(image_url)
    __base_url = '{SCHEME}://{HOST}'.format(SCHEME=__temp_parse_data.scheme, HOST=__temp_parse_data.netloc)
    url = urljoin(__base_url, __temp_parse_data.path)
    image_name = __temp_parse_data.query.split('/')[-1]
    image_save_path = ATTACHMENT_DATA_DIR_PATH.joinpath(image_name)
    if image_save_path.exists():
        logger.warning(f'图片{image_name}已存在,跳过...')
    else:
        temp = __temp_parse_data.query.split('=')
        payload = {
            temp[0]: temp[1]
        }
        message, error, response = http_download(url=url,
                                                 save_file=image_save_path.absolute(),
                                                 mime_type='image',
                                                 method='get',
                                                 params=payload
                                                 )
        if error:
            logger.error(f'下载{image_name}失败, {message} {error}')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

此方法中用到了ATTACHMENT_DATA_DIR_PATH变量,将会把图片下载到此目录下,后续读取图片也要使用此变量。

# 3.6.2.2 payload_func插件方法

此方法用于libra.yaml配置文件的fast_api_response配置模块,目的是构造每一个测试样本的请求体。

示例代码如下:

def payload_func(data: Dict) -> Dict:
    """
    对每一条测试样本的json数据进行处理,构造每条测试数据的请求体,返回一个dict
    相关示例说明见: http://172.16.114.1:7777/AITUtils/html/user/api.html#module-AITUtils.fast_http.FastHTTP
    """
    payload = dict()
    test_sample_image_id = data.get('imageId')
    image_url = data.get('url')
    __temp_parse_data = urlparse(image_url)
    image_name = __temp_parse_data.query.split('/')[-1]
    image_save_path = ATTACHMENT_DATA_DIR_PATH.joinpath(image_name).absolute()
    status, result_file_base64 = get_base64(image_save_path, is_file=True)
    # 成功获取图片base64
    if status:
        payload = {
            'noid': 111,
            'imageRows': [{
                'imageID': test_sample_image_id,
                'imageData': result_file_base64
            }]
        }
    else:
        logger.error(f'获取图片base64失败: {image_name}, {result_file_base64}')
    return payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

关于payload_func的详细说明示例,请查阅AITUtils文档:http://172.16.114.1:7777/AITUtils/html/user/api.html#module-AITUtils.fast_http.FastHTTP

# 3.6.2.3 analyzer插件方法

此方法用于libra.yaml配置文件的analyze配置模块,用于完成完成预测结果与标注结果的比对、分析、判定,构造每一条测试样本的分析结果数据结构,这是是我们代码评审的核心关注内容!

代码示例:

def analyzer(annotated_data, predicted_data) -> Dict:
    """
    对每一个样本的标注数据和预测结果数据进行分析,将分析结果构造为一个Dict
    :param annotated_data: 标注数据
    :param predicted_data: 预测结果
    :return:
    举个例子::
    >>> annotated_data = [
    >>>   {
    >>>     "box": [906, 431, 1007, 431, 1007, 559, 906, 559]
    >>>   },
    >>>   {
    >>>     "box": [548, 259, 650, 259, 650, 393, 548, 393]
    >>>   }
    >>> ]
    >>> predicted_data = [
    >>>   {
    >>>     "rect": {"left": 906, "top": 429, "right": 1006, "bottom": 554},
    >>>   },
    >>>   {
    >>>     "rect": {"left": 546, "top": 263, "right": 643, "bottom": 385},
    >>>   }
    >>> ]
    >>> result_data = {
    >>>     "expect_list": [
    >>>       {
    >>>         "head_completion": "yes_head_completion",
    >>>         "head_occlusion": "no_head_occlusion",
    >>>         "head_angle": "front",
    >>>         "box": [906, 431, 1007, 431, 1007, 559, 906, 559]
    >>>       },
    >>>       {
    >>>         "head_completion": "yes_head_completion",
    >>>         "head_occlusion": "existhead_occlusion",
    >>>         "head_angle": "front",
    >>>         "box": [548, 259, 650, 259, 650, 393, 548, 393]
    >>>       }
    >>>     ],
    >>>     "recognize_list": [
    >>>       {
    >>>         "rect": {"left": 906, "top": 429, "right": 1006, "bottom": 554}},
    >>>       {
    >>>         "rect": {"left": 546, "top": 263, "right": 643, "bottom": 385}
    >>>       }
    >>>     ],
    >>>     "correct_list": [
    >>>       {
    >>>         "head_completion": "yes_head_completion",
    >>>         "head_occlusion": "no_head_occlusion",
    >>>         "head_angle": "front",
    >>>         "box": [906, 431, 1007, 431, 1007, 559, 906, 559]
    >>>       },
    >>>       {
    >>>         "head_completion": "yes_head_completion",
    >>>         "head_occlusion": "existhead_occlusion",
    >>>         "head_angle": "front",
    >>>         "box": [548, 259, 650, 259, 650, 393, 548, 393]
    >>>       }
    >>>     ]
    >>>     "error_list": [],
    >>>     "loss_list": [],
    >>>     "one_to_more_list": [],
    >>>     "expect_count": 2,
    >>>     "recognize_count": 2,
    >>>     "correct_count": 2,
    >>>     "error_count": 0,
    >>>     "loss_count": 0,
    >>>     "status": "通过"
    >>> }
    """
    # 期望识别出来的结果列表
    expect_list = annotated_data
    # 识别出来的结果列表
    recognize_list = predicted_data
    # 识别正确的结果列表
    correct_list = list()
    # 识别错误的结果列表,有识别结果,但和期望结果不一致
    error_list = list()
    # 漏识别的结果列表,期望结果有值,实际没识别出来
    loss_list = list()
    # 一个期望匹配中多个响应的列表
    one_to_more_list = list()

    # 开始分析逻辑
    if len(recognize_list) == 0:
        correct_list = []
        error_list = []
        loss_list = expect_list
    else:
        __expect_list = deepcopy(expect_list)
        __recognize_list = deepcopy(recognize_list)

        for each_expect in expect_list:
            iou_match_dict = dict()
            iou_match_info_list = list()
            iou_match_dict_info = dict()
            # 逐个标注数据进行IOU计算判定
            each_expect_rect = each_expect['box']
            for each_actual in __recognize_list:
                each_actual_rect = rectangle_2point_to_4point(left_top=[each_actual['rect']['x1'],
                                                                        each_actual['rect']['y1']],
                                                              right_bottom=[each_actual['rect']['x2'],
                                                                            each_actual['rect']['y2']])
                iou = float(quadrilateral_iou(shape_actual=each_actual_rect, shape_expect=each_expect_rect))
                # 判定阈值,预测与标注的IOU大于0.5即正确
                if iou >= 0.5:
                    iou_match_dict.update({str(each_actual): iou})
            # 若仅匹配中一个,则此结果即为正确结果
            if len(iou_match_dict) == 1:
                correct_list.append(each_expect)
                __recognize_list.remove(eval(list(iou_match_dict.keys())[0]))
                __expect_list.remove(each_expect)
            # 若匹配中多个,取IOU最大的判定为正确结果,并记录所有匹配中的结果
            elif len(iou_match_dict) > 1:
                max_iou_head = max(iou_match_dict, key=lambda x: iou_match_dict[x])
                correct_list.append(each_expect)
                iou_match_info_list.extend([{'iou': v, 'pred_data': k} for k, v in iou_match_dict.items()])
                iou_match_dict_info.update(
                    {
                        'expect_head': each_expect,
                        'match_list': iou_match_info_list
                    }
                )
                one_to_more_list.append(iou_match_dict_info)
                __recognize_list.remove(max_iou_head)
                __expect_list.remove(each_expect)

        # 对于标注数据中没匹配中的人头,算作预测遗漏了
        if len(__expect_list) != 0:
            loss_list = __expect_list
        # 对于预测结果中多识别出来的人头,算作错误识别
        if len(__recognize_list) != 0:
            error_list = __recognize_list

    if expect_list == [] and error_list == [] and loss_list == []:
        status = '图片无人头'
    elif error_list == [] and loss_list == [] and len(expect_list) == len(correct_list):
        status = '通过'
    else:
        status = '不通过'

    return {
        "expect_list": expect_list,
        "recognize_list": recognize_list,
        "correct_list": correct_list,
        "error_list": error_list,
        "loss_list": loss_list,
        "one_to_more_list": one_to_more_list,
        "expect_count": len(expect_list),
        "recognize_count": len(recognize_list),
        "correct_count": len(correct_list),
        "error_count": len(error_list),
        "loss_count": len(loss_list),
        "status": status
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# 3.6.2.4 测试报表各插件方法

此部分自定义方法,用于libra.yaml配置文件的report配置模块,构建表头,表数据。

# 3.6.2.4.1 summary_statistic_data_list

此方法用于构建指标概览的统计数据,其统计数据已经根据libra.yaml的statistic配置模块的定义统计输出到statistic.json文件中了,并且在框架的数据预加载部分完成了对statistic.json的数据读取赋值给了statistic_data变量,因此我们直接操作statistic_data变量,即可完成所需数据结构的构建,示例代码如下:

def summary_statistic_data_list() -> List[List]:
    """
    获取指标概览数据
    :return:
    """
    # 构造指标概览数据结构示例,请根据情况修改
    data = [
        ['人头检测准确率',
         statistic_data['statistic_data']['head_accurate_rate']['result']['expression_value'],
         statistic_data['statistic_data']['head_accurate_rate']['result']['threshold_value'],
         statistic_data['statistic_data']['head_accurate_rate']['result']['status'],
         statistic_data['unit_data']['total_loss_count'],
         statistic_data['unit_data']['total_error_count'],
         statistic_data['unit_data']['total_correct_count'],
         statistic_data['unit_data']['total_recognize_count'],
         statistic_data['unit_data']['total_except_count']
         ],
        ['人头检测召回率',
         statistic_data['statistic_data']['head_recall_rate']['result']['expression_value'],
         statistic_data['statistic_data']['head_recall_rate']['result']['threshold_value'],
         statistic_data['statistic_data']['head_recall_rate']['result']['status'],
         statistic_data['unit_data']['total_loss_count'],
         statistic_data['unit_data']['total_error_count'],
         statistic_data['unit_data']['total_correct_count'],
         statistic_data['unit_data']['total_recognize_count'],
         statistic_data['unit_data']['total_except_count']
         ]
    ]
    return data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 3.6.2.4.2 detail_field_name_list

此方法用于构造分析详情汇总表的表头数据,其分析数据已经根据libra.yaml的analyze配置模块输出到analysis_result.jl文件中了,并且在框架的数据预加载部分完成了对``analysis_result.json的数据读取赋值给了analyze_data变量,因此我们直接操作analyze_data`变量,即可完成所需数据结构的构建,示例代码如下:

def detail_field_name_list() -> List[Text]:
    """
    构造分析详情汇总表的表头数据
    :return:
    """
    field_list = list(analyze_data[0].keys())
    field_list.append('image_status')
    return field_list
1
2
3
4
5
6
7
8
# 3.6.2.4.3 detail_data_list

此方法用于构造分析详情汇总的表内容数据,同上2.4.2小节,我们继续读取analyze_data变量的数据即可,示例代码如下:

def detail_data_list() -> List[List]:
    """
    构造分析详情汇总的表内容数据
    :return:
    """
    data_list = list()
    for record in analyze_data:
        record_data = list(record.values())
        record_data.append(record['analysis_result']['status'])
        data_list.append(record_data)
    return data_list
1
2
3
4
5
6
7
8
9
10
11
# 3.6.2.4.4 each_dim_statistic_data_list

此方法输出各维度分析内容数据,同2.4.1小节,我们直接使用statistic_data变量的数据即可,示例代码如下:

def each_dim_statistic_data_list() -> List[List]:
    """
    各维度分析内容数据
    :return:
    """
    data_list = list()
    dims = ['head_completion', 'head_occlusion', 'head_angle']
    for dim in dims:
        for each_statistic_key, each_statistic_value in statistic_data['statistic_data'].items():
            if dim in each_statistic_key:
                each_dim_name = each_statistic_key.replace('_recall_rate', '')
                line_data = [dim,
                             each_statistic_key,
                             each_statistic_value['result']['expression_value'],
                             each_statistic_value['result']['threshold_value'],
                             each_statistic_value['result']['status'],
                             statistic_data['unit_data'][f'total_correct_{each_dim_name}_count'],
                             statistic_data['unit_data'][f'total_expect_{each_dim_name}_count']
                             ]
                data_list.append(line_data)
    return data_list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#测试框架#智能算法测试
上次更新: 7/26/2026, 4:44:44 PM
AITDBClient使用文档
AITLibraManager埋点服务设计

← AITDBClient使用文档 AITLibraManager埋点服务设计→

最近更新
01
测试覆盖度量全景图
11-30
02
Prism测试覆盖度量
11-30
03
AITDBClient使用文档
11-15
更多文章>
Theme by Vdoing | Copyright © 2021-2026 toddlerya | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式