第3章〓EfficientDet与美食场景检测 当读完本章时,应该能够: 熟悉并理解美食数据集的结构特点。 了解解决目标检测问题的技术路线。 掌握一种为数据集做标签的方法。 理解并掌握EfficientDetD0~EfficientDetD7模型的体系结构与工作原理。 基于TFLite Model Maker做迁移学习。 基于TFLite Task Library在Android上部署TFLite模型。 基于mAP指标评价目标检测模型。 民以食为天,即刻拥有在美食领域创业的冲动与梦想。 3.1项目动力 美食是人类追求美好生活的应有之义。美食关系健康,例如,人体必需的八种氨基酸不能体内合成,需要从食物中摄取。在中国数千年的饮食文化岁月里,美食是区域文化符号,体现了区域特色,也体现了人们的创造与追求。中央电视台一度热播的纪录片《舌尖上的中国》将美食与健康、美食与文化、人们对美食的创造与演绎表达得淋漓尽致。 大千世界,美食多姿多彩,美食背后蕴含的知识也是海量的。如果人们在一起聚会聊天时,借助AI技术,对着餐桌上的美食拍一下,对那些即便不太熟悉的食材,也能迅速得知其产地习性、历史传承、营养成分、烹饪方法、饮食禁忌等知识,着实令人神往。 基于上述项目初心,本章案例将从零起步,从数据集的采集与标签定义,到EfficientDet模型解读,再到模型训练、评估、迁移、部署和应用,实现美食场景检测中最富创造力的一个环节,即自动区分食材类别。 正确界定食材类别是构建手机版美食应用的关键。关于食材的其他相关知识,可以通过构建数据库的方式完成,限于篇幅,数据库的设计不作为本章项目的内容。 3.2技术路线 目标检测主要有两种技术路线: 一种是TwoStage检测方法; 另一种是OneStage检测方法。 (1) TwoStage检测方法。将检测逻辑划分为两个阶段,首先产生候选区域,然后对候选区域进行校正和分类。这类算法的典型代表是基于候选区域的RCNN系列算法,如RCNN、Fast RCNN、Faster RCNN、Mask RCNN等。 (2) OneStage检测方法。不需要产生候选区域(Region Proposal)阶段,直接产生目标的坐标值和类别概率值,经典的算法如SSD、YOLO和EfficientDet等。 EfficientDet采用的骨干分类网络是EfficientNet,正如EfficientNet是一个系列模型(EfficientNetB0~EfficientNetB7),同时EfficientDet也是一个适应不同规模需求的模型系列,包括EfficientDetD0~EfficientDetD7。 图3.1显示了EfficientDet系列模型与其他目标检测模型在计算量与准确率两个维度上的对比。 图3.1EfficientDet与其他模型对比 EfficientDet在计算量与准确率两个指标上,显著领先于之前的其他经典模型。EfficientDetD0的准确率比YOLOv3稍高,但是计算量只有其1/28。从EfficientDetD4开始,在计算量相当或较低的情况下,其准确率已经显著领先于Mask RCNN、RetinaNet、ResNet+NASFPN等模型。 3.3MakeSense定义标签 本节介绍的数据集标注工具软件MakeSense是一款为数据集打标签的免费在线软件,不需要本地安装,入手简单,支持多种数据集格式。 MakeSense支持分类任务或者目标检测任务。输出的文件格式包括YOLO、VOC XML、VGG JSON和CSV等。对于目标检测问题,支持的标签类型包括点、线段、矩形框和多边形。官方网站工作地址为https://www.makesense.ai/。 在官方网站首页右下角有一个名称为Get Started的按钮,单击该按钮,打开工作界面,如图3.2所示,该界面提供了目标检测和图像识别两种工作模式。 将需要做标注的图片拖放到中央的大矩形框中,单击Object Detection按钮,首先会弹出一个询问界面,要求用户给定数据集标签列表,如图3.3所示,用户既可以一次性导入数据集的标签列表,也可以单击左上角的“+”按钮,临时定义标签列表。 图3.2MakeSense首页工作界面 图3.3定义数据集标签列表 创建标签列表后,即可为指定的图片做标签。可选择一批图片上传到MakeSense中,如图3.4所示,从左侧列表中选择图片,在中央工作区拖动鼠标,定义矩形框,框住目标,在中央工作区的右侧,右上角有标签选择栏,右下角有边界形状选择栏,共同确定本次标注内容的位置和类型。 本节视频教学中随机完成了5幅图像的标注工作,当完成全部图片标注时,单击MakeSense顶部导航栏Actions中的Export Annotations命令,弹出如图3.5所示的对话框,选择导出文件的格式,执行Export命令,导出数据集标签文件。 图3.4用MakeSense定义标签 图3.5导出数据集标签文件 打开数据集标签文件,内容如图3.6所示,其中只包含做过标注的图片,没有做标注的图片不在其中。 图3.6数据集标签文件结构 A列为标签的名称,B、C、D、E 4列依次是矩形框的左上角(x1, y1)与右下角(x2, y2)坐标,F列表示文件名称,G、H列分别表示图片的宽度与高度。 显然,当数据量很大时,数据标注是一项耗费人力和时间的工作。 3.4定义数据集 虽然可以采用3.3节的方法为数据集做标签,但是采集足够多的数据是一项富有挑战性的工作,事实上,本项目落地的一个前提即是构建超大的美食数据集。为了演示需要,本章项目采用的美食数据集来自UEC FOOD 100数据集,由日本电子通信大学食品识别研究小组发布,数据集下载地址为http://foodcam.mobi/dataset.html。 UEC FOOD 100数据集定义了100种美食对应的图片和标签。解压下载的数据集文件,目录列表如图3.7所示。每一种美食对应一个Bounding Box标签。 图3.7UEC FOOD 100数据集目录列表 以目录100为例,其包含的部分图片样本如图3.8所示。每一个目录均有一个名称为bb_info.txt的文件,存储该目录下所有图片的位置标签。文件bb_info.txt包含5列数据,依次是图像的ID(即文件名称),矩形框的左上角和右下角坐标x1、y1、x2、y2。 图3.8目录100包含的部分图片样本 数据集目录结构及功能描述如表3.1所示。 表3.1数据集目录结构与功能描述 目录或文件名称功 能 描 述样 本 规 模 目录1~100以数字1~100命名的100个目录,每个目录下存放同一种类型的美食图片,图片文件采用数字命名总样本数量为14611,类别总数为100 bb_info.txt存放于每一个目录下,记录该目录下每一幅图片的Bounding Box标签100个目录,共100个bb_info.txt文件 category.txt类别标签文件,100种类别对应的数字与英文名称100种类别的名称与索引 multiple_food.txt包含多个分类目标的图片id及其标签共1174幅图片 为了便于后续建模工作,上述数据集需要做进一步的预处理。用PyCharm打开本教材的项目TensorFlow_to_Android,在根目录下创建子目录EfficientDet,将图3.7所示的数据集目录dataset100移动到EfficientDet目录下。 在EfficientDet目录下新建程序dataset.py,完成数据集的划分与标签预处理工作,编码逻辑如程序源码P3.1所示。 程序源码P3.1dataset.py对数据集做预处理,划分训练集、验证集和测试集 1import numpy as np 2import pandas as pd 3from sklearn.utils import shuffle 4from PIL import Image 5all_foods = []# 存放所有样本标签 6# 读取所有类别名称 7category = pd.read_table('./dataset100/category.txt') 8# 列表中列的顺序 9column_order = ['type', 'img', 'label', 'x1', 'y1', 'x2', 'y2'] 10# 遍历目录 1~100,读取所有图片的标签信息,汇集到all_foods列表 11for i in range(1,101,1): 12# 读取当前目录i的标签信息 13foods = pd.read_table(f'./dataset100/{i}/bb_info.txt', 14header=0, 15sep='\s+') 16# 将图像ID映射为对应的文件路径 17foods['img'] = foods['img'].apply(lambda x: f'./dataset100/{i}/' + str(x) +'.jpg') 18# 新增一列 label,标注图片类别名称 19foods['label'] = foods.apply(lambda x: category['name'][i-1], axis=1) 20foods['type'] = foods.apply(lambda x: '', axis=1) 21foods = foods[column_order] 22# 保存当前类别的标签文件 23foods.to_csv(f'./dataset100/{i}/label.csv', 24index=None, 25header=['type', 'img', 'label', 'x1', 'y1', 'x2', 'y2']) 26# 汇聚到列表 all_foods 27all_foods.extend(np.array(foods).tolist()) 28# 保存列表到文件中 29df_foods = pd.DataFrame(all_foods) 30df_foods.to_csv('./dataset100/all_foods.csv', 31index=None, 32header=['type', 'img', 'label', 'x1', 'y1', 'x2', 'y2']) 33# 随机洗牌,打乱数据集排列顺序,划分为 TRAIN、VALIDATE、TEST三部分 34datasets = pd.read_csv('./dataset100/all_foods.csv') # 读数据 35datasets = shuffle(datasets,random_state=2022) # 洗牌 36datasets = pd.DataFrame(datasets).reset_index(drop=True) 37rows = datasets.shape[0] # 总行数 38test_n = rows //40 # 测试集样本数 39validate_n = rows //5 # 验证集样本数 40train_n = rows - test_n - validate_n # 训练集样本数 41print(f'测试集样本数:{test_n},验证集样本数:{validate_n},训练集样本数:{train_n}') 42# 按照一定比例对数据集进行划分 43for row in range(test_n): # 标注测试集 44datasets.iloc[row, 0] = 'TEST' 45for row in range(validate_n): # 标注验证集 46datasets.iloc[row + test_n, 0] = 'VALIDATE' 47for row in range(train_n): # 标注训练集 48datasets.iloc[row + test_n + validate_n, 0] = 'TRAIN' 49# 将Bounding Box的坐标改为浮点类型,取值范围为[0,1] 50print('开始对BBox坐标做归一化调整,请耐心等待...') 51for row in range(rows): 52img = Image.open(datasets.iloc[row, 1]) # 读取图像 53(width, height) = img.size # 图像宽度与高度 54width = float(width) 55height = float(height) 56datasets.iloc[row, 3] = round(datasets.iloc[row, 3] / width, 3) 57datasets.iloc[row, 4] = round(datasets.iloc[row, 4] / height, 3) 58datasets.iloc[row, 5] = round(datasets.iloc[row, 5] / width, 3 ) 59datasets.iloc[row, 6] = round(datasets.iloc[row, 6] / height, 3) 60datasets.insert(datasets.shape[1], 'Null1', '') # 插入空列 61datasets.insert(datasets.shape[1], 'Null2', '') # 插入空列 62# 调整列的顺序,为以后数据集划分做准备 63order = ['type', 'img', 'label', 'x1', 'y1', 'Null1', 'Null2', 'x2', 'y2'] 64datasets = datasets[order] 65print(datasets.head()) 66datasets.to_csv('./dataset100/datasets.csv', index=None, header=None) 67print('数据集构建完毕!') 运行程序dataset.py,查看dataset100目录下新生成的数据集文件datasets.csv,观察测试集样本数、验证集样本数和训练集样本数,可以根据实验环境的计算能力,适当调整数据集规模与比例划分。 程序源码P3.1的划分结果: 测试集样本数为365,验证集样本数为2922,训练集样本数为11324。 3.5EfficientDet解析 EfficientDet模型参见论文Efficientdet: Scalable and efficient object detection(TAN M,PANG R,LE Q V.2020),它是谷歌研究团队借鉴EfficientNet分类模型的体系架构,在目标检测领域取得的创新性进展。 EfficientDet的主要创新点有两个: 一是采用双向加权特征金字塔网络(a weighted bidirectional feature pyramid network, BiFPN),实现多尺度特征提取与融合; 二是采用复合缩放法,同时对所有主干网络、特征网络、目标定位网络和分类网络的分辨率、深度、宽度统一缩放。基于上述创新点,得到了EfficientDet模型系列,即EfficientDetD0~EfficientDetD7。 正如作者所强调的那样,EfficientDet模型的研发动力来自机器人、自动驾驶等对视觉模型精度和响应速度的严苛要求。机器视觉领域往往关注了速度,就会牺牲精度; 或者关注了精度,又会拖累速度。 EfficientDet的目标是在可伸缩架构和高精度之间取得平衡,开发更为高效的并适应多场景需求的目标检测网络,最终形成独特的网络设计,由主干网络、特征融合网络、定位网络和分类网络构成的EfficientDet模型如图3.9所示。 图3.9EfficientDet模型 EfficientDet模型从输入层开始,依次经历了主干网络、特征融合网络和分类/定位网络三个阶段,是一个端到端的网络结构。主干网络采用EfficientNet网络,特征融合网络采用BiFPN网络,分类和定位网络采用卷积网络。 来自目标检测场景的一个非常现实的问题是,有的目标看起来很大,有的目标看起来很小,同一种类型的目标由于观察距离或者视角的问题也会出现大小差异。如何处理这些大小不一的目标是一个挑战。有的模型可能对大目标识别度好,对小目标识别度差; 或者关注了小目标,大目标的误差又会偏大。对此,一种常见的解决方案是采用多尺度特征融合。EfficientDet在此基础上创新设计出了双向加权特征金字塔网络(BiFPN)并配合EfficientNet网络来更好地解决特征提取这个关键问题。 为了寻找最佳网络规模,研究发现,过往的模型只对主干网络和输入图像缩放,事实上对特征网络、定位网络和分类网络缩放也至关重要。对网络整体统一缩放,全局性更强。 EfficientNet强大的特征提取能力和分类能力及其匹配多种需求的优势,在本书第1章已经有系统的描述,此处不再赘述。 下面重点介绍EfficientDet的BiFPN技术和模型的复合缩放技术。图3.10给出了四种特征融合网络设计模式。 图3.10特征融合网络设计 图3.10(a)展示FPN,FPN对来自P3~P7的多尺度特征进行自顶向下的多尺度特征融合。图3.10(b)展示PANet,在FPN基础上叠加了自底向上的特征融合路径,即将FPN的单向特征融合变为双向特征融合。图3.10(c)展示NASFPN,它是基于机器自动学习模式搜索一个网络结构用于特征融合。图3.10(d)展示BiFPN,它是一种高效的双向跨尺度交叉连接和加权特征融合网络。 多尺度特征融合的前提是多尺度特征提取,图3.9展示了EfficientDet多尺度特征提取过程。主干网络采用EfficientNet,读者可以回看EfficientNetV2的模型结构(见表1.9),包含八层模块,去掉最后的输出层,EfficientNetV2的第1~7层是特征提取层,图3.9中的主干网给出的P1~P7,代表EfficientNetV2的七个模块层。但是在输入到特征融合网络时,只采用了其中的P3~P7这五个模块层进行特征融合。 以FPN为例,其特征融合逻辑可以表示为式(3.1)。 Pout7=Conv(Pin7) Pout6=Conv(Pin6+Resize(Pout7)) Pout5=Conv(Pin5+Resize(Pout6)) Pout4=Conv(Pin4+Resize(Pout5)) Pout3=Conv(Pin3+Resize(Pout4))(3.1) 再看BiFPN的特征融合逻辑。对于FPN、PANet而言,跨尺度特征之间叠加时,没有权重分配的问题,认为不同尺度的特征同等重要。而对于BiFPN,则采取了加权叠加方式,即认为不同尺度的特征,在特征融合时所占权重不同。 EfficientDet论文中给出了三种加权方法,分别如下。 (1) 无边界融合(Unbounded Fusion)。计算方法如式(3.2)所示。 O=∑iwi·Ii(3.2) 其中,wi表示对每一个输入Ii施加一个可学习的权重参数wi,区分不同尺度特征Ii的重要性后再叠加在一起,得到输出O。实验表明,这个加权方式缺乏模型稳定性,因为权重wi的取值自由度过大。 (2) 基于Softmax函数的融合(Softmaxbased Fusion)。计算逻辑如式(3.3)所示。 O=∑iewi∑jewj·Ii(3.3) 将权重wi用Softmax函数变换一下,约束到0~1这个区间内。基于Softmax的特征融合,将权重的取值限制为0~1,表达出了不同尺度特征的重要性,稳定性比无边界融合方法要好。但是实验表明,该方法明显拖累了GPU的运行速度。 (3) 快速归一化融合(Fast Normalized Fusion)。计算逻辑如式(3.4)所示。 O=∑iwiε+∑jwj·Ii(3.4) 显然,式(3.4)简化了式(3.3)的计算。实验表明,式(3.4)与式(3.3)在取得相似精度的前提下,GPU的计算速度提升了30%。 以BiFPN中的P6层的特征融合为例,其计算逻辑如式(3.5)所示。 Ptd6=Convw1·Pin6+w2·Resize(Pin7)w1+w2+ε Pout6=Convw′1·Pin6+w′2·Ptd6+w′3·Resize(Pout5)w′1+w′2+w′3+ε(3.5) 其中,Ptd6是P6层的中间计算结果,Pout6是P6层对应的最终输出结果。 观察图3.9的网络模型,不难看出BiFPN模块层往往需要重复多次,这就涉及最佳重复次数问题。 现在讨论模型的整体缩放。EfficientDet首先确定了一个基准模型。 对于主干网络,采用EfficientNetB0~EfficientNetB6作为基准参照。 对于BiFPN网络,其宽度与深度的缩放采用式(3.6)计算。 Wbifpn=64×(1.35),Dbifpn=3+(3.6) 其中,Wbifpn表示网络宽度(通道数),Dbifpn表示网络深度(层数)。参数1.35是通过对参数列表{1.2, 1.25, 1.3, 1.35, 1.4, 1.45}做网格搜索得到的最佳值。是缩放因子。 对于定位网络和分类网络,采用的缩放方法如式(3.7)所示。 Dbox=Dclass=3+/3(3.7) 输入图像分辨率的缩放如式(3.8)所示。 Rinput=512+128(3.8) 根据式(3.6)~式(3.8),用一个系数可以完成对整个EfficientDet网络的缩放,例如=0得到模型EfficientDetD0,=7得到模型EfficientDetD7,如表3.2所示。 表3.2EfficientDet系列模型参数 模型输入Rinput主干网络 BiFPN网络 WbifpnDbifpn定位网络和分 类网络Dclass D0(=0)512B06433 D1(=1)640B18843 D2(=2)768B211253 D3(=3)896B316064 D4(=4)1024B422474 D5(=5)1280B528874 D6(=6)1280B638485 D7(=7)1536B638485 D7x1536B738485 关于模型更多解析,参见本节视频教程。 3.6EfficientDetLite预训练模型 第2章的鸟类识别案例采用的是一种传统的TFLite建模方法,模型训练与部署路径: MobileNetV3建模→模型训练→模型评估→用TFLiteConverter将模型转换为TFLite版→添加TFLite元数据→将TFLite版模型部署应用到Android上。 本章案例尝试一种更为简单的方案,直接基于已经训练好的EfficientDetLite版模型做迁移学习,完成美食场景检测模型的训练与评估。技术路径: EfficientDetLite版预训练模型→用TensorFlow Lite Model Maker完成TFLite模型的训练与评估→得到迁移学习后的TFLite新模型→将TF Lite版模型部署应用到Android上。两种技术路径的对比关系如图3.11所示。 图3.11TFLite版模型建模路径 显然,两种建模路径的起点不同,基于TensorFlow Lite Model Maker库的建模路径,要求已经拥有预训练好的TFLite模型,而且TFLite模型训练完成后,不需要单独添加模型元数据信息,因为此前的EfficientDetLite预训练模型已经包含相关元数据的结构信息。 本章案例采用的EfficientDetLite预训练模型全部来自TensorFlow Hub,模型基于COCO 2017数据集训练,其性能表现如表3.3所示。 表3.3EfficientDetLite模型性能表现 Model ArchitectureSize/MBLatency/msAverage Precision/% EfficientDetLite04.43725.69 EfficientDetLite15.84930.55 EfficientDetLite27.26933.97 EfficientDetLite311.411637.70 EfficientDetLite419.926041.96 表3.3中数据来自TensorFlow Hub网站,其中: Size/MB: 表示采用整数量化后的模型大小。 Latency/ms: 表示模型在4核CPU的Pixel 4手机上的单幅图像的时间延迟。 Average Precision/%: 表示模型在COCO 2017验证集上的平均精度(mean Average Precision, mAP)。 以EfficientDetLite2模型为例,模型对输入图像的尺寸要求是: Height×Width×3,Height=448, Width=448, 像素取值范围为[0, 255]。 模型输出的内容如下。 (1) num_detections: 一次最多可检测的目标对象数量,最大值为25。 (2) detectionboxes: 定位目标的矩形框坐标。 (3) detectionclasses: 目标分类。 (4) detectionscores: 目标置信度。 为了便于读者学习,表3.3中的5个EfficientDetLite版预训练模型已经放到了本章项目文件夹EfficientDet\pretraining中。读者也可以自行到TensorFlow Hub官方网站下载。 3.7美食版EfficientDetLite训练 由于案例中使用了TensorFlow Lite Model Maker库和COCO 2017数据集的标签,因此需要在当前项目环境安装必需的软件包。用PyCharm打开当前项目,转到Terminal窗口,执行下述两条命令。 pip install tflite-model-maker pip install pycocotools 在当前项目EfficientDet根目录下创建程序model.py。基于迁移学习的模型训练逻辑如程序源码P3.2所示。 程序源码P3.2model.py美食版EfficientDetLite迁移学习训练 1import json 2from absl import logging 3from tflite_model_maker import model_spec 4from tflite_model_maker import object_detector 5import tensorflow as tf 6assert tf.__version__.startswith('2') 7tf.get_logger().setLevel('ERROR') 8logging.set_verbosity(logging.ERROR) 9spec = model_spec.get('efficientdet_lite0')# 指定模型 10print('数据集划分需要读取14611幅图像,可能花费几分钟时间。请耐心等待!') 11train_data, validation_data, test_data = \ 12object_detector.DataLoader.from_csv('./dataset100/datasets.csv') 13print('开始模型训练...') 14# 训练模型,指定训练参数 15model = object_detector.create(train_data, 16model_spec=spec, 17epochs=30, 18batch_size=16, 19train_whole_model=True, 20validation_data=validation_data) 21# 将训练好的模型导出为TFLite模型并保存到当前工作目录下。默认采用整数量化方法 22print('正在采用默认优化方法,保存TFLite模型...') 23model.export(export_dir='.') # 保存TFLite模型 24model.summary() 25# 保存与模型输出一致的标签列表 26classes = ['???'] * model.model_spec.config.num_classes 27label_map = model.model_spec.config.label_map 28for label_id, label_name in label_map.as_dict().items(): 29classes[label_id-1] = label_name 30print(classes) 31with open('labels.txt', 'w') as f: # 模型标签保存到文件 32for i in range(len(classes)): 33for label in classes: 34f.write(label+"\r") 35# 在测试集上评测训练好的模型 36dict1 = {} 37print('开始在测试集上对计算机版模型评估...') 38dict1 = model.evaluate(test_data, batch_size = 16) 39print(f'计算机版模型在测试集上评估结果:\n {dict1}') 40# 加载TFLite格式的模型,在测试集上做评估 41dict2 = {} 42print('开始在测试集上对优化后的TFLite模型评估...') 43dict2 = model.evaluate_tflite('model.tflite', test_data) 44print(f'优化后的TFLite模型在测试集上评估结果: \n {dict2}') 45# 保存模型的评估结果 46for key in dict1: 47dict1[key] = str(dict1[key]) 48print(f'{key}: {dict1[key]}') 49with open('dict1.txt','w') as f : 50f.write(json.dumps(dict1)) 51# 保存优化后的TFLite模型在测试集上的评估结果 52print('真实版的TFLite模型测试结果...') 53for key in dict2: 54dict2[key] = str(dict2[key]) 55print(f'{key}: {dict2[key]}') 56with open('dict2.txt','w') as f : 57f.write(json.dumps(dict2)) 运行程序model.py,数据集加载完成后,模型开始训练。注意,第17行语句和第18行语句指定的epochs参数和batch_size参数,可以根据配置的计算能力进行修改。如果内存低于32GB,建议将batch_size设置为8。 本章项目训练采用的主机配置如下。 (1) CPU: Intel Core i7,8核。 (2) RAM: 32GB。 (3) GPU: NVIDIA GeForce RTX 3070,8GB。 训练30代,大约需要3小时。读者可根据个人主机配置情况,调整模型训练参数。 训练过程演示及测试指标讲解参见本节视频教程。 3.8评估指标mAP 目标检测领域通常采用mAP作为模型评价的主要指标,例如Faster RCNN、SSD、EfficientDet、YOLO等算法均采用mAP指标作为模型的评价标准。目标检测领域有两个经典数据集,分别是Pascal VOC和MS COCO。mAP在这两个数据集上的计算逻辑有所区别,所以,有时会特别指出mAP遵循的计算方法,例如Pascal VOC的mAP指标或者MS COCO的mAP指标。 在COCO数据集关于mAP的解释中,通常将mAP与AP(Average Precision,平均精度)不做区分。AP的含义是当召回率(Recall Rate)在[0,1]这个区间变化时,对应的精确率(Precision Rate)的平均值。 显然,AP与精确率和召回率有关,那么什么是精确率和召回率呢? 以多分类问题中的类别A为例,精确率是预测结果正确的比例。精确率越高,意味着误报率越低,因此,当误报的成本较高时,精确率指标有助于判断模型的好坏。 召回率是正确预测的样本占该类样本总数的比例。召回率越高,意味着模型漏掉的目标越少,当漏掉的目标成本很高时,召回率指标有助于衡量模型的好坏。 精确率: Precision=预测结果为A且正确的数量预测结果为A的数量 召回率: Recall=预测结果为A且正确的数量类别A的总数量 要确定对某个目标对象的预测是否正确,通常采用IoU判断。IoU被定义为预测Bounding Box和实际Bounding Box的交集除以它们的并集。如果IoU>阈值,则认为预测正确; 如果IoU≤阈值,则认为预测错误。 当IoU>0.5的预测被认为是正确预测时,这意味着IoU=0.6或者IoU=0.9的两个预测具有相同的权重。因此,固定某个阈值会在评估指标中引入偏差。解决这个问题的一个思路是对一定范围内的IoU阈值,计算其mAP。 以COCO数据集上定义的mAP为例。当只考虑IoU阈值为0.5的情况时,平均精度记作AP50或者mAP50。同理,当只考虑IoU阈值为0.75时,平均精度可以记作AP75或者mAP75。 单个类别的平均精度通常添加一个表示类别名称的后缀。例如,米饭和鸡肉米饭两种美食的平均精度可以分别表示如下。 AP_/rice: 0.42938477 AP_/chicken rice: 0.7119283 COCO数据集上,mAP(或者AP)将IoU的阈值以0.05为步长,覆盖了[0.5:0.95]的10个数值,其计算逻辑如式(3.9)所示。 mAPCOCO=mAP0.50+mAP0.55+…+mAP0.9510(3.9) 事实上,mAP的计算逻辑包含三次平均计算过程。还是以COCO数据集采用的mAP为例。 步骤1: 对于每个类别(共80个类别),计算不同的IoU阈值下的AP,取它们的平均值,得到该类别的AP。计算逻辑如式(3.10)所示。 AP[class]=1#thresholds∑IoU∈thresholdsAP[class,IoU](3.10) 步骤2: 通过对不同类别的AP进行平均来计算最终的AP,如式(3.11)所示。 AP=1#classes∑class∈classesAP[class](3.11) 除了AP指标,COCO数据集上还定义了其他一些指标,用于反映模型的性能,如表3.4所示。 表3.4COCO数据集上反映模型性能的12个指标 指 标 名 称功 能 描 述 AP: 平均精度 AP最基本的评价指标,在IoU=0.50:0.05:0.95区间计算AP APIoU=0.50 固定IoU的阈值为0.50 APIoU=0.75固定IoU的阈值为0.75 AP Across Scales: 不同尺寸目标的平均精度 APsmall针对小目标(像素数量<322)的平均精度 APmedium 针对中目标(322<像素数量<962)的平均精度 APlarge针对大目标(像素数量>962)的平均精度 Average Recall(AR): 平均召回率 ARmax=1每幅图像最多给出1个检测目标 ARmax=10 每幅图像最多给出10个检测目标 ARmax=100每幅图像最多给出100个检测目标 AR Across Scales: 不同尺寸目标的平均召回率 ARsmall针对小目标(像素数量<322)的平均召回率 ARmedium 针对中目标(322<像素数量<962)的平均召回率 ARlarge针对大目标(像素数量>962)的平均召回率 3.9美食版EfficientDetLite评估 3.7节模型训练程序model.py中已经给出了12个综合评价指标(见表3.4)以及每个类别(共100个类别)的平均精度值。 为便于直观观察模型效果,程序evaluation.py完成了EfficientDetLite计算机版与移动版TFLite模型之间的对比。 选取了如下三个比较维度: (1) 计算机版EfficientDetLite与移动版EfficientDetLite的12项指标对比。 (2) 按照各类别的AP排序,前20名AP对比。 (3) 按照各类别的AP排序,后20名AP对比。 在当前项目中新建程序evaluation.py,编程逻辑如程序源码P3.3所示。 程序源码P3.3evaluation.py美食版EfficientDetLite模型评估 1import json 2import pandas as pd 3import numpy as np 4import seaborn as sns 5import matplotlib.pyplot as plt 6# 读取计算机版TFLite模型评估数据 7dict1 = [json.loads(line) for line in open(r'dict1.txt','r')] 8for key in dict1[0]: 9dict1[0][key] = float(dict1[0][key]) 10df1 = pd.DataFrame(dict1) 11print(df1.head()) 12# 读取移动版TFLite模型评估数据 13dict2 = [json.loads(line) for line in open(r'dict2.txt','r')] 14for key in dict2[0]: 15dict2[0][key] = float(dict2[0][key]) 16df2 = pd.DataFrame(dict2) 17print(df2.head()) 18# 取前12项指标 19columns = ['AP', 'AP50', 'AP75', 'APs', 'APm', 'APl', 20'ARmax1', 'ARmax10', 'ARmax100', 'ARs','ARm','ARl'] 21df1_12 = df1.iloc[0, 0:12] 22df2_12 = df2.iloc[0, 0:12] 23sns.barplot(x=np.array(df1_12).tolist(), y=columns)# 计算机版TFLite 24plt.show() 25sns.barplot(x=np.array(df2_12).tolist(), y=columns) # 移动版TFLite 26plt.show() 27# 100个类别mAP指标的条形图 28df1.drop(columns=columns, inplace=True, axis=1) 29df1 = df1.stack() # 行列互换 30df1 = df1.unstack(0) 31df1.sort_values(by=0, axis=0, ascending=False, inplace=True) 32df2.drop(columns=columns, inplace=True, axis=1) 33df2 = df2.stack() # 行列互换 34df2 = df2.unstack(0) 35df2.sort_values(by=0, axis=0, ascending=False, inplace=True) 36# 根据需要,只显示mAP值最高的前20个类别 37sns.barplot(x=df1[0][0:20], y=df1.index[0:20]) # 计算机版TFLite 38plt.show() 39sns.barplot(x=df2[0][0:20], y=df2.index[0:20]) # 移动版TFLite 40plt.show() 41# 只显示mAP值最低的20个类别 42sns.barplot(x=df1[0][-20:], y=df1.index[-20:]) # 计算机版TFLite 43plt.show() 44sns.barplot(x=df2[0][-20:], y=df2.index[-20:]) # 移动版TFLite 45plt.show() 执行程序源码P3.3,观察计算机版TFLite模型与移动版TFLite模型的对比效果。 图3.12给出了计算机版模型的12项指标条形图分布。各指标含义参见表3.4。 图3.12计算机版TFLite模型的12项指标条形图分布 图3.13给出了移动版TFLite模型的12项指标条形图分布。 图3.13移动版TFLite模型的12项指标条形图分布 图3.12和图3.13中,APs(小目标平均精确率)和ARs(小目标平均召回率)的值均为-1,表示测试集中不存在小目标图像。 不难看出,由于移动版TFLite模型做了量化优化,各项指标值均低于计算机版TFLite模型。追求计算速度的同时,损失精确率与召回率在所难免。但是移动版TFLite模型仍然整体上保持了较高的精确率和召回率,例如其AP50超过了0.6。 图3.14给出了计算机版TFLite模型前20名类别的AP条形图分布,AP值均超过0.8。 图3.14计算机版TFLite模型前20名类别的AP条形图分布 图3.15给出了移动TFLite模型前20名类别的AP条形图分布,虽然依旧保持了较高的AP值,但是排名顺序发生变化,而且目标对象并不完全一致。 图3.15移动版TFLite模型前20名类别的AP条形图分布 图3.14中有4种美食AP_/Japanesestyle pancake、AP_/pork cutlet on rice、AP_/sushi、AP_/sashimi bowl不包括在图3.15中,取而代之的是另外4种美食AP_/tempura、AP_/hamburger、AP_/spicy chiliflavored tofu、AP_/dipping noodles。而且即使对于同一种美食,其排名顺序也不一定相同。以AP_/pizza为例,在图3.15中排在第20名,而在图3.14中,却排到了第3名。 观察前20种美食的排名,计算机版TFLite模型与移动版TFLite模型差异显著。可见,量化优化以后,对模型精度的影响是显著的。 图3.16给出了计算机版TFLite模型后20名类别的AP条形图分布。检查数据集文件dataset.csv发现,AP_/fried fish在测试集中包含3个样本,AP_/vegetable tempura在测试集中只包含1个样本,故其AP值非常小。测试集中不包含AP_/sauteed vegetables和AP_/croissant,故其AP值为-1。 图3.16计算机版TFLite模型后20名类别的AP条形图分布 图3.17给出了移动版TFLite模型后20名类别的AP条形图分布。移动版TFLite的排名,除了类别列表上的差异,有更多的类别的AP值接近于0,这说明这些类别在测试集中的样本数量过少,同时也说明,对比计算机版TFLite,移动TFLite版在精度上的损失显著增加了。 图3.17移动版TFLite模型后20名类别的AP条形图分布 事实上,程序源码P3.1随机划分数据集时,对于100个类别来讲,测试集只包含365个样本,平均每个类别3.65个样本,确实太少了。这也是为了增强教学演示效果、说明相关问题而刻意为之的一个举措。就本章案例而言,测试集和验证集的样本数各占2000左右,训练集为10000左右比较合理。 3.10美食版EfficientDetLite测试 在将移动版TFLite模型部署到Android手机上之前,首先在计算机里对模型做样本实证观察。在当前项目中新建程序predict.py,编码逻辑如程序源码P3.4所示。 程序源码P3.4predict.py美食版EfficientDetLite模型测试 1import cv2 2import tensorflow as tf 3from PIL import Image 4import numpy as np 5model_path = 'model.tflite'# 预训练模型 6with open('labels.txt','r') as f: # 读取模型标签文件 7classes = f.readlines() 8for i in range(len(classes)): # 取出标签中的换行符 9classes[i] = classes[i].replace('\n','') 10# 图像预处理 11def preprocess_image(image_path, input_size): 12img = tf.io.read_file(image_path) # 读取指定图像 13img = tf.io.decode_image(img, channels=3) # 解码 14img = tf.image.convert_image_dtype(img, tf.uint8) # 数据类型 15original_image = img # 原始图像 16resized_img = tf.image.resize(img, input_size) # 图像缩放 17resized_img = resized_img[tf.newaxis, :] # 增加维度,表示样本数量 18resized_img = tf.cast(resized_img, dtype=tf.uint8) # 数据类型 19return resized_img, original_image # 裁剪后的图像与原始图像 20def detect_objects(interpreter, image, threshold): 21""" 22用指定的模型和置信度阈值,对指定的图像检测 23:param interpreter: 推理模型 24:param image: 待检测图像 25:param threshold: 置信度阈值 26:return: 返回检测结果(字典列表) 27""" 28# 推理模型解释器 29signature_fn = interpreter.get_signature_runner() 30# 对指定图像做目标检测 31output = signature_fn(images=image) 32# 解析检测结果 33count = int(np.squeeze(output['output_0'])) # 检测到的目标数量 34scores = np.squeeze(output['output_1']) # 置信度 35class_curr = np.squeeze(output['output_2']) # 类别 36boxes = np.squeeze(output['output_3']) # Bounding Box坐标 37results = [] 38for i in range(count): # 所有目标组织为列表 39if scores[i] >= threshold: # 只返回超过阈值的目标 40result = {# 以字典格式组织单个检测结果 41'bounding_box': boxes[i], 42'class_id': class_curr[i], 43'score': scores[i] 44} 45results.append(result) 46return results # 返回检测结果(字典列表) 47def run_odt_and_draw_results(image_path, interpreter, threshold=0.5): 48""" 49用指定模型在指定图片上根据阈值做目标检测并绘制检测结果 50:param image_path: 待检测图像 51:param interpreter: 推理模型 52:param threshold: 置信度阈值 53:return: 绘制Bounding Box、类别和置信度的图像数组 54""" 55# 根据模型获得输入维度 56_, input_height, input_width, _ = interpreter.get_input_details()[0]['shape'] 57# 加载图像并做预处理 58preprocessed_image, original_image = preprocess_image( 59image_path, 60(input_height, input_width) 61) 62# 对图像做目标检测 63results = detect_objects(interpreter, 64preprocessed_image, 65threshold=threshold) 66# 在图像上绘制检测结果(Bounding Box,类别,置信度) 67original_image_np = original_image.numpy().astype(np.uint8) 68for obj in results: 69# 根据原始图像尺寸(高度和宽度),将Bounding Box的坐标调整为整数 70ymin, xmin, ymax, xmax = obj['bounding_box'] 71xmin = int(xmin * original_image_np.shape[1]) 72xmax = int(xmax * original_image_np.shape[1]) 73ymin = int(ymin * original_image_np.shape[0]) 74ymax = int(ymax * original_image_np.shape[0]) 75# 当前类别的ID 76class_id = int(obj['class_id']) 77# 用指定颜色绘制Bounding Box 78color = [0,255,0] # 颜色 79cv2.rectangle(original_image_np, 80(xmin, ymin), 81(xmax, ymax), 82color, 1) 83# 调整类别标签的纵向坐标,保持可见 84y = ymin - 5 if ymin - 5 > 15 else ymin + 20 85# 类别标签和置信度显示为字符串 86label = "{}: {:.0f}%".format(classes[class_id], obj['score'] * 100) 87color = [255,255,0] # 标签文本颜色 88cv2.putText(original_image_np, label, (xmin+5, y), 89cv2.FONT_ITALIC, 0.5, color, 1) # 绘制标签 90# 返回绘制结果的图像 91original_uint8 = original_image_np.astype(np.uint8) 92return original_uint8 93# 随机选择图像进行测试 94# TEMP_FILE = './dataset100/25.jpg' 95TEMP_FILE = './dataset100/11156.jpg' 96DETECTION_THRESHOLD = 0.13 # 置信度阈值,可以调整 97im = Image.open(TEMP_FILE) # 打开图像 98im.thumbnail((512, 512), Image.ANTIALIAS) # 缩放 99im.save(TEMP_FILE) # 保存缩放后的图像 100# 加载TFLite推理模型 101interpreter = tf.lite.Interpreter(model_path=model_path) 102interpreter.allocate_tensors() 103# 进行目标检测并绘制检测结果 104detection_result_image = run_odt_and_draw_results( 105TEMP_FILE, 106interpreter, 107threshold=DETECTION_THRESHOLD 108) 109# 显示检测结果 110Image.fromarray(detection_result_image).show() 运行程序predict.py,修改第96行语句设定的置信度阈值,观察输出结果。图3.18所示为图片25.jpg在置信度阈值为0.13时的测试结果。 图3.19所示为图片11156.jpg在置信度阈值为0.13时的测试结果。注意,其中的french fries检测到了两个目标框,因为其置信度阈值均超过了0.13。 图3.18图片25.jpg在置信度阈值为 0.13时的测试结果(5种美食 全部检出)(见彩插) 图3.19图片11156.jpg在置信度阈值为 0.13时的测试结果(检测到4种 美食) 3.11新建Android项目 新建Android项目,项目模板选择Empty Activity,项目名称为Foods,项目包可自由定义,本章设置为cn.edu.ldu.foods,编程语言选择Kotlin,SDK最小版本号设置为API 21: Android 5.0(Lollipop),如图3.20所示,单击Finish按钮,完成项目创建和初始化。 图3.20项目初始化与参数配置 打开项目资源列表中的strings.xml文件,修改app_name属性的值为“美食场景检测”: <string name="app_name">美食场景检测</string> 右击项目视图中的app节点,在弹出的快捷菜单中执行New→Image Asset命令,在弹出的对话框中选择一幅素材图片作为程序图标,调整图标大小,完成图标定制,如图3.21所示。 图3.21定制项目图标 选择app节点,在鼠标右键的快捷菜单中执行New→Folder→Assets Folder命令,创建assets目录,复制model.tflite模型文件到项目的assets目录下。 执行Refactor→Migrate to AndroidX命令,将项目支持库转变为AndroidX模式。 在项目的build.gradle文件中,添加TFLite Task Library库依赖: implementation 'org.tensorflow:tensorflow-lite-task-vision:0.3.1' 在AndroidManifest.xml清单文件开启相机拍照功能。 <queries> <intent> <action android:name="android.media.action.IMAGE_CAPTURE" /> </intent> </queries> 在AndroidManifest.xml中添加provider元素,定义FileProvider。 <manifest> ... <application> ... <provider android:name="androidx.core.content.FileProvider" android:authorities="cn.edu.ldu.objectdetection.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application> </manifest> 在res/xml节点下创建file_paths.xml文件,定义外部存储路径。 <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-files-path name="my_images" path="Pictures" /> </paths> 此时,项目结构如图3.22所示。其中清单文件AndroidManifest.xml、模块依赖文件build.gradle、照片存储路径文件file_paths.xml和TFLite模型文件model.tflite已经部署完成。 图3.22项目结构 接下来的工作是完成界面设计和主程序逻辑设计。 3.12Android界面设计 打开activity_main.xml文件,定义界面布局,如图3.23所示。自顶向下包含四个控件,依次是文本提示控件tvPlaceholder、图像视图控件imageView、相机控件btnCapture、相册控件btnPicture。图3.24为模拟器测试的初始界面。 图3.23界面布局设计 图3.24模拟器测试的初始界面 界面脚本如程序源码P3.5所示。 程序源码P3.5activity_main.xml界面布局设计 1<?xml version="1.0" encoding="utf-8"?> 2<androidx.constraintlayout.widget.ConstraintLayout 3xmlns:android="http://schemas.android.com/apk/res/android" 4xmlns:app="http://schemas.android.com/apk/res-auto" 5xmlns:tools="http://schemas.android.com/tools" 6android:layout_width="match_parent" 7android:layout_height="match_parent" 8tools:context=".MainActivity"> 9<FrameLayout 10android:layout_width="match_parent" 11android:layout_height="match_parent" 12android:layout_above="@+id/btnCamera" 13app:layout_constraintStart_toStartOf="parent" 14app:layout_constraintTop_toTopOf="parent"> 15<TextView 16android:id="@+id/tvPlaceholder" 17android:layout_width="match_parent" 18android:layout_height="wrap_content" 19android:layout_marginTop="10dp" 20android:text="此处显示检测结果" 21android:textAlignment="center" 22android:textSize="36sp" /> 23<ImageView 24android:id="@+id/imageView" 25android:layout_width="match_parent" 26android:layout_height="464dp" 27android:adjustViewBounds="true" 28android:contentDescription="@null" 29android:scaleType="fitCenter" 30app:srcCompat="@mipmap/ic_launcher_foreground" /> 31</FrameLayout> 32<Button 33android:id="@+id/btnCamera" 34android:layout_width="100dp" 35android:layout_height="80dp" 36android:layout_marginStart="60dp" 37android:layout_marginBottom="60dp" 38android:background="@android:drawable/ic_menu_camera" 39app:layout_constraintBottom_toBottomOf="parent" 40app:layout_constraintStart_toStartOf="parent" 41tools:ignore="SpeakableTextPresentCheck" /> 42<Button 43android:id="@+id/btnPicture" 44android:layout_width="90dp" 45android:layout_height="70dp" 46android:layout_marginEnd="60dp" 47android:layout_marginBottom="65dp" 48android:background="@android:drawable/ic_menu_gallery" 49app:layout_constraintBottom_toBottomOf="parent" 50app:layout_constraintEnd_toEndOf="parent" 51tools:ignore="SpeakableTextPresentCheck" /> 52</androidx.constraintlayout.widget.ConstraintLayout> 在模拟器或者真机上做项目测试,此时,只能看到初始界面,单击“相机”按钮和“相册”按钮,系统没有响应。 3.13Android逻辑设计 本节完成主程序MainActivity.kt的编程设计,程序逻辑如图3.25所示,包括10个模块函数和一个实体类,矩形框内为模块函数名称或实体类名称,旁边给出了模块功能的简单描述。 图3.25程序逻辑 模块之间的箭头连线表示了其调用关系。虚线箭头表示不是直接调用关系,但存在间接的逻辑关联或者事件关联。 虚线框表示该模块是系统函数模块,需要重写或者调用。实线框表示该模块是由用户新定义完成的模块。 编码逻辑如程序源码P3.6所示,其中模块名称和实体类名称加了粗体标注。 程序源码P3.6MainActivity.kt程序主逻辑 1package cn.edu.ldu.foods 2import android.app.Activity 3import android.content.ActivityNotFoundException 4import android.content.Intent 5import android.graphics.* 6import android.net.Uri 7import androidx.appcompat.app.AppCompatActivity 8import android.os.Bundle 9import android.os.Environment 10import android.provider.MediaStore 11import android.util.Log 12import android.view.View 13import android.widget.Button 14import android.widget.ImageView 15import android.widget.TextView 16import androidx.core.content.FileProvider 17import androidx.exifinterface.media.ExifInterface 18import androidx.lifecycle.lifecycleScope 19import kotlinx.coroutines.Dispatchers 20import kotlinx.coroutines.launch 21import org.tensorflow.lite.support.image.TensorImage 22import org.tensorflow.lite.task.vision.detector.Detection 23import org.tensorflow.lite.task.vision.detector.ObjectDetector 24import java.io.File 25import java.io.IOException 26import java.text.SimpleDateFormat 27import java.util.* 28import kotlin.math.max 29import kotlin.math.min 30class MainActivity : AppCompatActivity(), View.OnClickListener { 31companion object {// 定义常量 32const val TAG = "TFLite Object Detection" 33const val REQUEST_IMAGE_CAPTURE: Int = 2022 34private const val MAX_FONT_SIZE = 96F 35} 36private lateinit var btnCamera: Button 37private lateinit var inputImageView: ImageView 38private lateinit var tvPlaceholder: TextView 39private lateinit var currentPhotoPath: String 40override fun onCreate(savedInstanceState: Bundle?) { 41super.onCreate(savedInstanceState) 42setContentView(R.layout.activity_main) 43btnCamera = findViewById(R.id.btnCamera) 44inputImageView = findViewById(R.id.imageView) 45tvPlaceholder = findViewById(R.id.tvPlaceholder) 46btnCamera.setOnClickListener(this) 47} 48// 相机拍照返回后的回调函数 49override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 50super.onActivityResult(requestCode, resultCode, data) 51if (requestCode == REQUEST_IMAGE_CAPTURE && 52resultCode == Activity.RESULT_OK 53) { 54setViewAndDetect(getCapturedImage()) // 显示检测结果 55} 56} 57// onClick(v: View?), 检测Activity上的单击事件 58override fun onClick(v: View?) { 59when (v?.id) { 60R.id.btnCamera -> { 61try { 62dispatchTakePictureIntent() 63} catch (e: ActivityNotFoundException) { 64Log.e(TAG, e.message.toString()) 65} 66} 67} 68} 69// 目标检测函数,完成对指定图像的目标检测 70private fun runObjectDetection(bitmap: Bitmap) { 71// Step 1: 创建 TFLite's TensorImage 对象 72val image = TensorImage.fromBitmap(bitmap) 73// Step 2: 初始化目标检测器对象 74val options = ObjectDetector.ObjectDetectorOptions.builder() 75.setMaxResults(5) 76.setScoreThreshold(0.3f) // 更改置信度阈值,会影响检测结果 77.build() 78val detector = ObjectDetector.createFromFileAndOptions( 79this, 80"model.tflite", // 此前训练好的TFLite模型文件 81options 82) 83// Step 3: TensorImage格式的图像传给检测器,开始检测 84val results = detector.detect(image) 85// Step 4: 分析检测结果并显示 86val resultToDisplay = results.map { 87// 获取排名第一的类别,构建显示文本 88val category = it.categories.first() 89val text = "${category.label}, ${category.score.times(100).toInt()}%" 90// 创建数据对象,存储检测结果 91DetectionResult(it.boundingBox, text) 92} 93// 在位图上绘制检测结果 94val imgWithResult = drawDetectionResult(bitmap, resultToDisplay) 95// 将检测结果更新到视图 96runOnUiThread { 97inputImageView.setImageBitmap(imgWithResult) 98} 99} 100// 将图像显示到视图中,并对其做目标检测 101private fun setViewAndDetect(bitmap: Bitmap) { 102// 显示图像 103inputImageView.setImageBitmap(bitmap) 104tvPlaceholder.visibility = View.INVISIBLE // 隐藏文本提示 105// 目标检测是一个同步过程,为避免界面阻塞,将检测过程定义为协程模式 106lifecycleScope.launch(Dispatchers.Default) { runObjectDetection(bitmap) } 107} 108// 对相机返回的图像解码并根据图像视图的大小进行裁剪 109private fun getCapturedImage(): Bitmap { 110// 视图的宽度与高度 111val targetW: Int = inputImageView.width 112val targetH: Int = inputImageView.height 113val bmOptions = BitmapFactory.Options().apply { 114inJustDecodeBounds = true 115BitmapFactory.decodeFile(currentPhotoPath, this) 116// 图像的宽度与高度 117val photoW: Int = outWidth 118val photoH: Int = outHeight 119// 计算裁剪比例因子 120val scaleFactor: Int = max(1, min(photoW / targetW, photoH / targetH)) 121inJustDecodeBounds = false 122inSampleSize = scaleFactor 123inMutable = true 124} 125// 获取照片的属性信息 126val exifInterface = ExifInterface(currentPhotoPath) 127val orientation = exifInterface.getAttributeInt( 128ExifInterface.TAG_ORIENTATION, 129ExifInterface.ORIENTATION_UNDEFINED 130) 131val bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions) 132return when (orientation) { // 根据照片方向做适当旋转变换 133ExifInterface.ORIENTATION_ROTATE_90 -> { 134rotateImage(bitmap, 90f) 135} 136ExifInterface.ORIENTATION_ROTATE_180 -> { 137rotateImage(bitmap, 180f) 138} 139ExifInterface.ORIENTATION_ROTATE_270 -> { 140rotateImage(bitmap, 270f) 141} 142else -> { 143bitmap 144} 145} 146} 147// 对图像进行旋转变换 148private fun rotateImage(source: Bitmap, angle: Float): Bitmap { 149val matrix = Matrix() 150matrix.postRotate(angle) 151return Bitmap.createBitmap( 152source, 0, 0, source.width, source.height, 153matrix, true 154) 155} 156// 创建图像文件,为相机拍摄的照片写入做准备 157@Throws(IOException::class) 158private fun createImageFile(): File { 159// 图像文件名称及其路径 160val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) 161val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES) 162return File.createTempFile( 163"JPEG_${timeStamp}_", /* prefix */ 164".jpg", /* suffix */ 165storageDir /* directory */ 166).apply { 167// 返回图像文件保存路径 168currentPhotoPath = absolutePath 169} 170} 171// 调用相机拍照 172private fun dispatchTakePictureIntent() { 173Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent -> 174// 确保有 camera activity 处理 intent 175takePictureIntent.resolveActivity(packageManager)?.also { 176// 创建存储相机数据的图像文件 177val photoFile: File? = try { 178createImageFile() 179} catch (e: IOException) { 180Log.e(TAG, e.message.toString()) 181null 182} 183// 如果文件创建成功 184photoFile?.also { 185val photoURI: Uri = FileProvider.getUriForFile( 186this, 187"cn.edu.ldu.objectdetection.fileprovider", 188it 189) 190// 保存图像 191takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) 192// 回传相机拍照结果 193startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE) 194} 195} 196} 197} 198// 绘制检测结果,包括Bounding Box、类别名称、置信度 199private fun drawDetectionResult( 200bitmap: Bitmap, 201detectionResults: List<DetectionResult> 202): Bitmap { 203val outputBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true) 204val canvas = Canvas(outputBitmap) 205val pen = Paint() 206pen.textAlign = Paint.Align.LEFT 207detectionResults.forEach { 208// 绘制 Bounding Box 209pen.color = Color.GREEN 210pen.strokeWidth = 8F 211pen.style = Paint.Style.STROKE 212val box = it.boundingBox 213canvas.drawRect(box, pen) 214val tagSize = Rect(0, 0, 0, 0) 215// 字体设置 216pen.style = Paint.Style.FILL_AND_STROKE 217pen.color = Color.YELLOW 218pen.strokeWidth = 2F 219pen.textSize = MAX_FONT_SIZE 220pen.getTextBounds(it.text, 0, it.text.length, tagSize) 221val fontSize: Float = pen.textSize * box.width() / tagSize.width() 222// 调整字体大小,让文本显示在框内 223if (fontSize < pen.textSize) pen.textSize = fontSize 224var margin = (box.width() - tagSize.width()) / 2.0F 225if (margin < 0F) margin = 0F 226canvas.drawText( 227it.text, box.left + margin, 228box.top + tagSize.height().times(1F), pen 229) 230} 231return outputBitmap // 返回绘制检测结果的图像 232} 233} 234// 实体类,存储检测到的对象的可视化信息 235data class DetectionResult(val boundingBox: RectF, val text: String) 第70~99行实现的函数模块runObjectDetection,基于TFLite Task Library编写Android目标检测逻辑,通过4个步骤轻松完成,确实非常简单。第96行语句用线程模式更新界面,第106行语句用协程模式完成后台目标检测的推理过程,避免界面阻塞。 程序源码P3.6略去了从相册选择图片做目标检测的逻辑设计,该项功能也留到本章的课后习题,读者不难根据照相机的逻辑设计,自行完成相册的目标检测。更多解释参见本节视频讲解。 3.14Android手机测试 修改程序源码P3.6的第76行语句setScoreThreshold(0.13f)的置信度阈值参数,可以影响返回的检测结果。 为了与3.10节的程序源码P3.4做对照,这里仍然采用0.13的阈值参数,并且选取25.jpg和11156.jpg作为教学演示图片。使用Android真机测试,检测结果分别如图3.26和图3.27所示。 图3.2625.jpg图像置信度阈值为 0.13的检测结果 图3.2711156.jpg图像置信度阈值为 0.13的检测结果 图3.26与图3.18均采用25.jpg图像做目标检测测试。图3.26所示的Android真机检测只发现了3个正确的目标,而在置信度阈值同为0.13的情况下,图3.18给出的正确检测结果是5个。事实上,可以把这种差异归结为手机对着屏幕拍照时屏幕的反光、抖动或其他光影效果对成像质量的影响造成的。 图3.27与图3.19均采用11156.jpg图像做目标检测测试。有意思的是,仍然是对着屏幕拍照,在置信度阈值相同的情况下,图3.27的检测效果却更好,显示检出了5个目标,比图3.21多出了jiaozi。同时,对于汤品的认定也不同,图3.27给出的结果是chinese soup,而图3.19给出的结果是miso soup。 表3.5给出的实证测试对比,从一定程度上说明EfficientDet模型的健壮性好,泛化能力强。 表3.5两种场景检测效果对比(置信度阈值为0.13) 类别真机对屏幕场景TFLite对图片场景 rice17%,正确检测37%,正确检测 cold tofu31%,正确检测21%,正确检测 miso soup27%,chinese soup34%,正确检测 french fries29%,正确检测27%,正确检测 jiaozi19%,正确检测没有检测到 图3.28是将置信度阈值调整为0.1后的检测结果,与图3.26做对比,虽然多出了jiaozi这个目标,然而并不正确,只是说明了阈值对结果的影响。图3.29给出了正确的检测结果。 图3.28置信度阈值为0.1的检测结果 图3.29置信度阈值为0.2的检测结果 3.15小结 本章以美食场景中的食材检测为切入点,以EfficientDet模型应用于美食场景检测的方法路径为主线,完成了基于MakeSense的数据集标注、EfficientDet论文深度解析、基于TensorFlow Lite Model Maker实现TFLite模型的训练与评估、基于TFLite Task Library实现Android版的美食场景检测。 3.16习题 1. 目标检测常见的技术路线有哪些? 2. 如何为目标检测数据集定义标签?试举例说明。 3. EfficientDet模型的主要创新点包括哪些? 4. 双向加权特征金字塔网络(BiFPN)与其他特征融合模式相比优势有哪些? 5. EfficientDet模型的复合缩放方法是如何实现的? 6. EfficientDet与哪些经典模型做了对比?论文给出的实验结论是什么? 7. 描述EfficientDet的结构,解析这种结构的优势。 8. TFLite版模型建模路径有哪些? 9. 描述EfficientDetLite版模型做迁移学习的基本步骤。 10. 目标检测问题为什么会选择mAP作为评估指标? 11. 描述mAP指标的计算逻辑。 12. 计算机版TFLite模型与移动版TFLite模型的差异说明了什么问题? 13. 结合美食版EfficientDetLite模型的建模、训练与测试,侧重从健康美食的角度谈谈你对美食类App应用前景的瞻望。 14. 结合本章项目设计,谈谈在Android上部署应用TFLite模型的方法和步骤。 15. 美食版TFLIte模型与鸟类版TFLite模型部署有何不同? 16. 根据相机拍照的检测逻辑设计,自行完成相册的目标检测设计。