前言
本节以安全帽检测任务,采用YOLOv5模型,实现在Win7上的模型部署和C++动态链接库调用加载。大致流程包括YOLOv5目标检测模型训练和导出、C++动态链接库编译生成、Python加载调用动态链接库、Win7上进行部署运行。整体系统流程框架如下图所示。
一、模型的训练和导出
该节进行基于YOLOv5版本的PyTorch模型训练和转换导出,采用GPU训练,CPU导出。
(1)YOLOv5训练目标检测模型(GPU训练)
1.环境配置
训练环境选用Win11深度学习工作站,具体硬件和软件配置环境如下表:
工作站配置 | 类型 | 软件环境安装 | 版本 |
操作系统 | Win 11 家庭中文版64位 | 编程语言 | Anaconda3+Python 3.8.8 |
CPU | i7-13700KF | GPU加速库 | CUDA 11.1.0 + cuDNN 8.0.4.30 |
GPU | NVIDIA Geforce RTX 3080 | 深度学习框架 | PyTorch 1.8.0 + TorchVision 0.9.0 |
内存 | 32GB | 计算机视觉库 | OpenCV-Python 4.1.2.30 |
- | - | 推理框架 | Onnx 1.12.0 + ONNX Runtime 1.12.0 |
2.制作训练数据集
本文使用的安全帽数据集总共包含有7581张图像,数据集名称为Safety_Helmet,数据集共包含两种类型,分别为hat和person。数据集下有Annotations、JPEGImages和TXT文件,其中Annotations为标注的xml文件;JPEGImages为原图,包含有.jpg和.JPG类型;TXT为转换后的.txt文件。
安全帽数据集Safety_Helmet的百度云下载链接如下,该数据集大小1GB多,若下载速度慢可下载小型数据集:
- 安全帽数据集Safety_Helmet(未划分训练和测试样本)
- 链接:https://pan.baidu.com/s/1yZJGjfiYmLJzaTR3HbHXYg
- 提取码:t4lc
若该数据集下载过慢的话,这里准备了一个安全帽小型数据集Safety_Helmet_Small,该数据集共有800张图像,总大小94M,下载链接如下:
- 安全帽小型数据集Safety_Helmet_Small(未划分训练和测试样本)
- 链接:https://wwf.lanzouj.com/iryBX1xvncab
- 密码:avk5
若对数据集不想手动划分的话,在本节最后给出了划分后的安全帽数据集和安全帽小型数据集。
①这里先给出.xml文件转为.txt文件的脚本代码xml_txt.py:
参考CSDN: https://blog.csdn.net/weixin_43387635/article/details/130307679
# 导入相关库
import os
from lxml import etree
from tqdm import tqdm
def voc2txt():
# 获取xml文件夹下的所有xml文件名,存入列表
xmls_list = os.listdir(xmls_path)
for xml_name in tqdm(xmls_list):
# 打开写入文件
txt_name = xml_name.replace('xml', 'txt')
f = open(os.path.join(txts_save_path, txt_name), 'w') # 代开待写入的txt文件
with open(os.path.join(xmls_path, xml_name), 'rb') as fp:
# 开始解析xml文件
xml = etree.HTML(fp.read())
width = int(xml.xpath('//size/width/text()')[0])
height = int(xml.xpath('//size/height/text()')[0])
# 获取对象标签
obj = xml.xpath('//object')
for each in obj:
name = each.xpath("./name/text()")[0]
classes = dic[name]
xmin = int(each.xpath('./bndbox/xmin/text()')[0])
xmax = int(each.xpath('./bndbox/xmax/text()')[0])
ymin = int(each.xpath('./bndbox/ymin/text()')[0])
ymax = int(each.xpath('./bndbox/ymax/text()')[0])
# 归一化
dw = 1 / width
dh = 1 / height
x_center = (xmin + xmax) / 2
y_center = (ymax + ymin) / 2
w = (xmax - xmin)
h = (ymax - ymin)
x, y, w, h = x_center * dw, y_center * dh, w * dw, h * dh
# 写入
f.write(str(classes) + ' ' + str(x) + ' ' + str(y) + ' ' + str(w) + ' ' + str(h) + ' ' + '\n')
f.close() # 关闭txt文件
if __name__ == '__main__':
dic = {'hat': "0", # 创建字典用来对类型进行转换
'person': "1" # 此处的字典要与自己的classes.txt文件中的类对应,且顺序要一致
}
xmls_path = r"safety_helmet\\Annotations\\" # xml文件所在的文件夹
txts_save_path = r"Safety_Helmet/TXT" # txt文件所在的文件夹
os.mkdir(txts_save_path) if not os.path.exists(txts_save_path) else None
voc2txt()
修改xmls_path为数据集下的xml路径,txts_save_path为保存输出的.txt文件路径,运行该程序需要提前安装好tqdm等依赖库,运行完成后自动生成.txt文件
②安全帽的数据集如下,其中数据集已经包括转换为.txt的文件:
③之后对数据集进行划分,这里只划分为训练集(train)和验证集(val),不对测试集(test)进行划分,划分比例为8:2,划分的脚本yolo_split.py如下:
import os, shutil, random
from tqdm import tqdm
'''
分割训练集,验证集,测试集
断点为需要修改的路径地址
'''
def split_img(img_path, label_path, split_list):
try: # 创建数据集文件夹
Data = 'Safety_Helmet_YOLO' # 此处必须使用相对路径
os.mkdir(Data)
train_img_dir = Data + '/images/train'
val_img_dir = Data + '/images/val'
test_img_dir = Data + '/images/test'
train_label_dir = Data + '/labels/train'
val_label_dir = Data + '/labels/val'
test_label_dir = Data + '/labels/test'
# 创建文件夹
os.makedirs(train_img_dir)
os.makedirs(train_label_dir)
os.makedirs(val_img_dir)
os.makedirs(val_label_dir)
os.makedirs(test_img_dir)
os.makedirs(test_label_dir)
except:
print('文件目录已存在')
train, val, test = split_list
all_img = os.listdir(img_path)
all_img_path = [os.path.join(img_path, img) for img in all_img]
# all_label = os.listdir(label_path)
# all_label_path = [os.path.join(label_path, label) for label in all_label]
train_img = random.sample(all_img_path, int(train * len(all_img_path)))
train_img_copy = [os.path.join(train_img_dir, img.split('\\')[-1]) for img in train_img]
train_label = [toLabelPath(img, label_path) for img in train_img]
train_label_copy = [os.path.join(train_label_dir, label.split('\\')[-1]) for label in train_label]
for i in tqdm(range(len(train_img)), desc='train ', ncols=80, unit='img'):
_copy(train_img[i], train_img_dir)
_copy(train_label[i], train_label_dir)
all_img_path.remove(train_img[i])
val_img = random.sample(all_img_path, int(val / (val + test) * len(all_img_path)))
val_label = [toLabelPath(img, label_path) for img in val_img]
for i in tqdm(range(len(val_img)), desc='val ', ncols=80, unit='img'):
_copy(val_img[i], val_img_dir)
_copy(val_label[i], val_label_dir)
all_img_path.remove(val_img[i])
test_img = all_img_path
test_label = [toLabelPath(img, label_path) for img in test_img]
for i in tqdm(range(len(test_img)), desc='test ', ncols=80, unit='img'):
_copy(test_img[i], test_img_dir)
_copy(test_label[i], test_label_dir)
def _copy(from_path, to_path):
shutil.copy(from_path, to_path)
def toLabelPath(img_path, label_path):
img = img_path.split('\\')[-1]
if img[-4:] == '.jpg':
label = img.split('.jpg')[0] + '.txt'
elif img[-4:] == '.JPG':
label = img.split('.JPG')[0] + '.txt'
return os.path.join(label_path, label)
def main():
img_path = r'.\\Safety_Helmet\\JPEGImages'
label_path = r'.\\Safety_Helmet\\TXT'
split_list = [0.8, 0.2, 0.0] # 数据集划分比例[train:val:test]
split_img(img_path, label_path, split_list)
if __name__ == '__main__':
main()
修改main函数中图像路径img_path和txt文件路径label_path,输出Safety_Helmet_YOLO为划分数据集的文件夹。
注意:由于数据集中包含有两种类型图片(.jpg和.JPG),因此在toLabelPath函数中,对图片的后缀文件名进行了相应的判断后再分割,用于其它数据时需要对应修改,此处修改的代码如下:
if img[-4:] == '.jpg':
label = img.split('.jpg')[0] + '.txt'
elif img[-4:] == '.JPG':
label = img.split('.JPG')[0] + '.txt'
如程序运行过程中报错,可删除Safety_Helmet_YOLO文件夹后重新执行程序进行划分。
这里给出划分样本后的安全帽数据集Safety_Helmet_YOLO,该数据集较大,若下载速度慢可下载安全帽小型数据集:
- 安全帽数据集Safety_Helmet_YOLO(已划分训练和测试样本)
- 链接:https://pan.baidu.com/s/1Av2z6PLRnWNxvIEF7ZxaTg
- 提取码:j1mq
同时给出整理划分样本后的安全帽小型数据集Safety_Helmet_Small,该数据集可直接添加到程序中进行训练:
- 安全帽小型数据集Safety_Helmet_Small(已划分训练和测试样本)
- 链接:https://wwf.lanzouj.com/iKaXN1xvt1vc
- 密码:9j2d
3.训练模型
本文使用基于YOLOv5两类的安全帽目标检测模型,hat表示佩戴安全帽,person表示没有安全帽。更复杂的安全帽检测模型可以参考这个CSDN,总共有六种类别的安全帽检测。
下面使用YOLOv5模型进行训练、导出。对此步骤熟悉的话可以跳过,下文中也给出了训练完成后的安全帽模型.pt和导出转换模型.onnx的下载链接。
①下载GitHub官方的YOLOv5模型——YOLOv5官方源码,并一同下载训练权重,YOLOv5用于目标检测的预训练权重包括有YOLOv5n、YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x,本文使用YOLOv5s权重,这里给出YOLOv5n、YOLOv5s和YOLOv5m权重的下载链接:
- YOLOv5目标检测预训练权重模型
- 链接:https://wwf.lanzouj.com/iZPVS1tsdk8f
- 密码:1wbh
②由于YOLOv5在GitHub上的代码一直都在不断更新,最新下载的YOLOv5包需要按照说明要求安装相应的库。本文使用7.0版本的YOLOv5程序,这里给出YOLOv5修改后的程序代码,代码中相关配置文件的程序已经修改,并放置好了权重文件yolov5s.pt。
- YOLOv5程序(已修改配置文件程序,有yolov5s预训练权重,无数据集)
- 链接:https://wwf.lanzouj.com/i4Cq51wgyhpg
- 密码:i1kf
代码中需要加入安全帽数据集Safety_Helmet_YOLO,将Safety_Helmet_YOLO数据集放置于yolov5-master/data 文件夹下面即可进行训练。
下面详细说明对代码修改的每一处,对此步骤熟悉可以直接略过[Ⅰ]~[Ⅲ]。
[Ⅰ] 修改models/yolov5s.yaml 文件配置,修改参数nc为2,表示类别为两类,其它的不变。
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Parameters
nc: 2 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
[Ⅱ] 修改data/coco128.yaml文件,修改数据集路径path和类名names。
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: data/Safety_Helmet_YOLO # dataset root dir
train: images/train # train images (relative to 'path') 128 images
val: images/val # val images (relative to 'path') 128 images
test: # test images (optional)
# Classes
names:
0: hat
1: person
# Download script/URL (optional)
download: https://ultralytics.com/assets/coco128.zip
[Ⅲ] 修改train.py中parse_opt函数的参数,根据自身情况合理设置参数。本文指定cfg路径为models/yolov5s.yaml,epochs设置为200步,batch_size批大小设置为8,workers工作进程设置为1,指定选用GPU训练,下面展示了修改后的参数:
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='models/yolov5s.yaml', help='model.yaml path')
parser.add_argument('--epochs', type=int, default=200, help='total training epochs')
parser.add_argument('--batch-size', type=int, default=8, help='total batch size for all GPUs, -1 for autobatch')
parser.add_argument('--device', default='0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--workers', type=int, default=1, help='max dataloader workers (per RANK in DDP mode)')
③运行训练程序train.py,在cmd中yolov5-master的路径下执行python train.py,终端打印输出,开始训练模型,如下图所示。程序有可能会报其它的错误,比如“OSError:页面文件太小,无法完成操作”,但只要不影响训练过程就没关系。
在RTX 3080显卡上训练200步用了18小时训练完成,得到权重模型文件best.pt,权重文件大小约14M,如下图:
(2)模型导出格式转换(CPU导出)
1.导出转换onnx
训练完成后进行权重模型文件的导出,把.pt导出转换为.onnx模型文件,导出过程采用CPU进行导出。在export.py文件中,修改.pt的文件路径和指定转换输出格式为onnx。
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / r'runs/train/exp/weights/best.pt', help='model.pt path(s)')
parser.add_argument(
'--include',
nargs='+',
default=['onnx'],
# default=['engine'],
help='torchscript, onnx, openvino, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle')
在终端执行python export.py进行模型的导出,得到best.onnx文件,权重大小约为27.1M。
下面给出了训练完成后的安全帽模型best.pt和导出转换模型best.onnx的下载链接:
- 权重模型.pt和.onnx文件
- 链接:https://wwf.lanzouj.com/ixBz71tsdekb
- 密码:aowv
2.模型测试验证
下面编写一个简单的Python脚本文件加载onnx模型并推理,运行onnx_inference.py文件:
参考CSDN: https://blog.csdn.net/qq128252/article/details/127105463
import os
import cv2
import numpy as np
import onnxruntime
import time
CLASSES=['hat', 'person'] #安全帽检测类别
class YOLOV5():
def __init__(self,onnxpath):
self.onnx_session=onnxruntime.InferenceSession(onnxpath)
self.input_name=self.get_input_name()
self.output_name=self.get_output_name()
#-------------------------------------------------------
# 获取输入输出的名字
#-------------------------------------------------------
def get_input_name(self):
input_name=[]
for node in self.onnx_session.get_inputs():
input_name.append(node.name)
return input_name
def get_output_name(self):
output_name=[]
for node in self.onnx_session.get_outputs():
output_name.append(node.name)
return output_name
#-------------------------------------------------------
# 输入图像
#-------------------------------------------------------
def get_input_feed(self,img_tensor):
input_feed={}
for name in self.input_name:
input_feed[name]=img_tensor
return input_feed
#-------------------------------------------------------
# 1.cv2读取图像并resize
# 2.图像转BGR2RGB和HWC2CHW
# 3.图像归一化
# 4.图像增加维度
# 5.onnx_session 推理
#-------------------------------------------------------
def inference(self,img_path):
img=cv2.imread(img_path)
or_img=cv2.resize(img,(640,640))
img=or_img[:,:,::-1].transpose(2,0,1) #BGR2RGB和HWC2CHW
img=img.astype(dtype=np.float32)
img/=255.0
img=np.expand_dims(img,axis=0)
input_feed=self.get_input_feed(img)
pred=self.onnx_session.run(None,input_feed)[0]
return pred,or_img
#dets: array [x,6] 6个值分别为x1,y1,x2,y2,score,class
#thresh: 阈值
def nms(dets, thresh):
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
#-------------------------------------------------------
# 计算框的面积
# 置信度从大到小排序
#-------------------------------------------------------
areas = (y2 - y1 + 1) * (x2 - x1 + 1)
scores = dets[:, 4]
keep = []
index = scores.argsort()[::-1]
while index.size > 0:
i = index[0]
keep.append(i)
#-------------------------------------------------------
# 计算相交面积
# 1.相交
# 2.不相交
#-------------------------------------------------------
x11 = np.maximum(x1[i], x1[index[1:]])
y11 = np.maximum(y1[i], y1[index[1:]])
x22 = np.minimum(x2[i], x2[index[1:]])
y22 = np.minimum(y2[i], y2[index[1:]])
w = np.maximum(0, x22 - x11 + 1)
h = np.maximum(0, y22 - y11 + 1)
overlaps = w * h
#-------------------------------------------------------
# 计算该框与其它框的IOU,去除掉重复的框,即IOU值大的框
# IOU小于thresh的框保留下来
#-------------------------------------------------------
ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)
idx = np.where(ious <= thresh)[0]
index = index[idx + 1]
return keep
def xywh2xyxy(x):
# [x, y, w, h] to [x1, y1, x2, y2]
y = np.copy(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2
y[:, 1] = x[:, 1] - x[:, 3] / 2
y[:, 2] = x[:, 0] + x[:, 2] / 2
y[:, 3] = x[:, 1] + x[:, 3] / 2
return y
def filter_box(org_box,conf_thres,iou_thres): #过滤掉无用的框
#-------------------------------------------------------
# 删除为1的维度
# 删除置信度小于conf_thres的BOX
#-------------------------------------------------------
org_box=np.squeeze(org_box)
conf = org_box[..., 4] > conf_thres
box = org_box[conf == True]
#-------------------------------------------------------
# 通过argmax获取置信度最大的类别
#-------------------------------------------------------
cls_cinf = box[..., 5:]
cls = []
for i in range(len(cls_cinf)):
cls.append(int(np.argmax(cls_cinf[i])))
all_cls = list(set(cls))
#-------------------------------------------------------
# 分别对每个类别进行过滤
# 1.将第6列元素替换为类别下标
# 2.xywh2xyxy 坐标转换
# 3.经过非极大抑制后输出的BOX下标
# 4.利用下标取出非极大抑制后的BOX
#-------------------------------------------------------
output = []
for i in range(len(all_cls)):
curr_cls = all_cls[i]
curr_cls_box = []
curr_out_box = []
for j in range(len(cls)):
if cls[j] == curr_cls:
box[j][5] = curr_cls
curr_cls_box.append(box[j][:6])
curr_cls_box = np.array(curr_cls_box)
# curr_cls_box_old = np.copy(curr_cls_box)
curr_cls_box = xywh2xyxy(curr_cls_box)
curr_out_box = nms(curr_cls_box,iou_thres)
for k in curr_out_box:
output.append(curr_cls_box[k])
output = np.array(output)
return output
def draw(image,box_data):
#-------------------------------------------------------
# 取整,方便画框
#-------------------------------------------------------
boxes=box_data[...,:4].astype(np.int32)
scores=box_data[...,4]
classes=box_data[...,5].astype(np.int32)
for box, score, cl in zip(boxes, scores, classes):
top, left, right, bottom = box
print('class: {}, score: {}'.format(CLASSES[cl], score))
print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(top, left, right, bottom))
cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2)
cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),(top, left ),cv2.FONT_HERSHEY_SIMPLEX,0.6, (0, 0, 255), 2)
if __name__=="__main__":
onnx_path='best.onnx'
model=YOLOV5(onnx_path)
output,or_img=model.inference('Safety_Helmet\\JPEGImages\\000000.jpg')
outbox=filter_box(output,0.65,0.45)
draw(or_img,outbox)
#cv2.imwrite('res.jpg',or_img)
cv2.imshow('show',or_img)
cv2.waitKey(0)
运行该文件,输出检测结果用OpenCV可视化,正常显示表明模型加载正常,如下图:
二、动态链接库编译生成
该节使用C++编程实现对动态链接库dll可执行文件的生成,包括软件环境安装配置、Visual Studio的C++程序编写和可执行文件的生成。
(1)软件环境安装配置
动态链接库的生成需要安装好相应的开发工具,包括Visual Studio、OpenCV和ONNX Runtime,已安装好的可跳过本小节。本节基于C++编程语言,使用的相关安装包的版本如下表:
编程语言 | C++ |
操作系统 | Windows 11 |
开发工具 | Visual Studio 2017+ VC 15 |
图像处理库 | OpenCV 4.1.2 |
推理框架 | ONNX Runtime 1.5.1 |
1.Visual Studio安装
开发工具常用Microsoft Visual Studio进行C++程序的开发,使用VS2017版本进行后续开发,VS2017专业版和企业版的在线安装链接如下,包里附有激活码,推荐安装专业版:
- Visual studio 2017在线安装包
- 百度云链接:https://pan.baidu.com/s/1_97zeAzNB998D3LZ-Kw9TA
- 提取码:7wbs
- 蓝奏云链接:https://wwf.lanzouj.com/i3RZT1uy40uf
- 密码:6jcr
2.OpenCV安装
下载安装C++版本的图像视觉库OpenCV,官方下载连接:https://opencv.org/releases/ ,下载指定Windows版本,下载后进行解压提取,这里给出OpenCV 4.1.2版本的分享下载链接,下载后解压至非中文路径下:
- OpenCV 4.1.2 安装包
- 链接:https://pan.baidu.com/s/1T4fhOlBGh6sArRh3eVcFyg
- 提取码:t35b
3.ONNX Runtime安装
下载C++版本的ONNX Runtime推理框架,官方下载链接:https://github.com/microsoft/onnxruntime/releases/ ,
推荐下载低版本ONNX Runtime,这里在win7系统上部署用基于CPU的64位OnnxRuntime 1.5.1版本,如下图所示,下载后解压至非中文路径下:
- ONNX Runtime 1.5.1 安装包
- 链接:https://wwf.lanzouj.com/i1Faq1topqxe
- 密码:be8q
4.创建动态链接库项目
①新建项目。打开VS软件,在文件栏中新建项目,如下图所示。在Visual C++下的Windows桌面中,选中动态链接库(DLL),名称起名为detection,点击确定后完成创建一个动态链接库项目。
②配置OpenCV库。右方的解决方案资源管理器中,打开项目配置属性,选中VC++目录,分别在包含目录和库目录下添加OpenCV下的include路径和lib路径,如下图,完整详细路径如下:
- 包含目录:opencv\build\include
- 库目录:opencv\build\x64\vc15\lib
接着,在链接器下的输入中,附加依赖项添加opencv_world412.lib文件,如下图,该文件在OpneCV中的完整参考路径如下:
- opencv_world412.lib文件路径:opencv\build\x64\vc15\lib\opencv_world412.lib
③配置ONNX Runtime库。在项目配置属性中,选中C/C++目录,在附加包含目录中添加onnxruntime的include路径;选中链接器目录,在链接器下的附加库目录中添加lib路径,如下图所示,完整详细路径如下:
- 包含目录:onnxruntime-win-x64-1.5.1\include
- 库目录:onnxruntime-win-x64-1.5.1\lib
接着,在链接器下的输入中,附加依赖项添加onnxruntime.lib文件,如下图,该文件在ONNX Runtime中的完整参考路径如下:
- onnxruntime.lib文件路径:onnxruntime-win-x64-1.5.1\lib\onnxruntime.lib
添加配置完成后,分别创建头文件detection.h和源文件detection.cpp,并指定配置为Release模型下的x64平台,如下图所示,若使用Debug或者x86的需要更换相应的配置文件,否则会报错。
最后,在头文件中写入以下代码,代码编译后若不报错则表明库文件添加配置成功,若报错需要重新添加配置:#include <iostream> #include<fstream> #include<opencv2/opencv.hpp> #include <onnxruntime_cxx_api.h>
(2)VS中的C++编程
在C++编写程序中,主要定义了以下两个函数,动态链接库的导出函数和图像重设大小函数,其中作为动态链接库的导出函数是object_detection,函数输入图像路径和模型路径,函数输出为自定义结构体类型stru_detection。两个函数的具体信息如下表:
函数 动态导出函数 图像重设大小函数 函数名称 object_detection resize_image 输入参数 ① image_path:图像路径(const char * ) ① srcimg:原始图像(cv::Mat) ② model_path:模型路径(const wchar_t * ) ② neww和newh :重设高度和宽度大小,默认为640 - ③ padh和padw:填充高度和宽度像素值,默认为640 输出参数 ① value:判断是否检测成功(int) dstimg:重设大小后的图像(cv::Mat) ② pred_image:预测结果图像(uchar*) - ③ height和weight:图像的高度和宽度(int) 1.头文件编程(.h)
在detection.h头文件中,导入相应的库,并进行函数和导出的动态链接库函数的声明,编写代码如下:
#pragma once #include <iostream> #include<fstream> #include <string.h> #include<opencv2/opencv.hpp> #include <onnxruntime_cxx_api.h> using namespace cv; using namespace std; //图像高度和宽度缩放比 double r; //声明重设大小函数 cv::Mat resize_image(cv::Mat srcimg, int* newh, int* neww, int* top, int* left); //结构体,用于动态链接库的返回,包括检测状态值和检测图像 struct stru_detection { int value = 0; //判断是否存在特殊情况 //(图片读取失败 - 1, 模型加载失败 - 2, 未检测到 - 3, 其它情况0) uchar* pred_image; //返回检测后图像 int height, weight; //返回图像的高度和宽度 }; //动态链接库中导出该函数 extern "C" _declspec(dllexport) stru_detection object_detection(const char * image_path, const wchar_t* model_path);
上述代码中,object_detection为需要导出的动态链接库函数,其返回值为自定义结构体类型stru_detection,包括四个参数——value用于判断图像是否检测成功, pred_image为预测后的图像,height和weight分别为输入图像的高度和宽度。
2.源代码文件编程(.cpp)
在源代码文件detection.cpp中,进行详细的程序编写实现目标检测的功能,并返回相应的值,包括模型加载推理,图片加载预测等,编写代码如下:
#include "pch.h" #include "detection.h" #include <vector> stru_detection object_detection(const char* image_path, const wchar_t* model_path) { //创建自定义结构体yolo_detection stru_detection yolo_detection; //创建实例 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "yolov5s"); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(1); session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED); try //判断模型是否存在 { Ort::Session session(env, model_path, session_options); } catch (const std::exception& ex) { //模型不存在返回value为-1 yolo_detection.value = -1; return yolo_detection; } Ort::Session session(env, model_path, session_options); // print model input layer (node names, types, shape etc.) Ort::AllocatorWithDefaultOptions allocator; //输入和输出模型节点名称 size_t num_input_nodes = session.GetInputCount(); std::vector<const char*> input_node_names = { "images" }; std::vector<const char*> output_node_names = { "output0" }; size_t input_tensor_size = 3 * 640 * 640; std::vector<float> input_tensor_values(input_tensor_size); //读取图像 cv::Mat srcimg = cv::imread(image_path); //判断图像是否存在 if (srcimg.empty()) { //图像不存在返回value为-2 yolo_detection.value = -2; return yolo_detection; } int newh = 0, neww = 0, padh = 0, padw = 0; //容器数组存放类名 std::vector<const char*> class_names = { "hat","person" }; //dstimg为重设后的图像大小 [640 x 640] cv::Mat dstimg = resize_image(srcimg, &newh, &neww, &padh, &padw);// 重设图像大小640×640×3 //像素值归一化并将输入的BGR转成RGB float ratioh = (float)srcimg.rows / newh, ratiow = (float)srcimg.cols / neww; for (int c = 0; c < 3; c++) { for (int i = 0; i < 640; i++) { for (int j = 0; j < 640; j++) { float pix = dstimg.ptr<uchar>(i)[j * 3 + 2 - c]; input_tensor_values[c * 640 * 640 + i * 640 + size_t(j)] = pix / 255.0; } } } //创建输入张量 std::vector<int64_t> input_node_dims = { 1, 3, 640, 640 }; auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), input_node_dims.size()); std::vector<Ort::Value> ort_inputs; ort_inputs.push_back(std::move(input_tensor)); // score model & input tensor, get back output tensor std::vector<Ort::Value> output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), ort_inputs.data(), input_node_names.size(), output_node_names.data(), output_node_names.size()); //获取指向输出浮点值张量的指针 const float* rawOutput = output_tensors[0].GetTensorData<float>(); //generate proposals std::vector<int64_t> outputShape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape(); size_t count = output_tensors[0].GetTensorTypeAndShapeInfo().GetElementCount(); //count:151200 std::vector<float> output(rawOutput, rawOutput + count); std::vector<cv::Rect> boxes; std::vector<float> confs; std::vector<int> classIds; int numClasses = (int)outputShape[2] - 5; int elementsInBatch = (int)(outputShape[1] * outputShape[2]); //25200*7=176400 //std::cout << "输出形状:" << output.size() << std::endl << "类别数量:" << numClasses << std::endl; //置信度阈值 float confThreshold = 0.65; // (1, 25200, 7) outputShape[2]:7 for (auto it = output.begin(); it != output.begin() + elementsInBatch; it += outputShape[2]) { float clsConf = *(it + 4);//object scores if (clsConf > confThreshold) { int centerX = (int)(*it); int centerY = (int)(*(it + 1)); int width = (int)(*(it + 2)); int height = (int)(*(it + 3)); //int x1 = centerX - width / 2; //int y1 = centerY - height / 2; int x1 = (centerX - padw - width / 2.0)*ratiow; int y1 = (centerY - padh - height / 2.0)*ratiow; width = width / r; height = height / r; //在容器末尾添加一个新的元素 (左上方x和y坐标,boxes的高和宽) boxes.emplace_back(cv::Rect(x1, y1, width, height)); // first 5 element are x y w h and obj confidence int bestClassId = -1; float bestConf = 0.0; for (int i = 5; i < numClasses + 5; i++) { if ((*(it + i)) > bestConf) { bestConf = it[i]; bestClassId = i - 5; } } //confs.emplace_back(bestConf * clsConf); confs.emplace_back(clsConf); classIds.emplace_back(bestClassId); } } //iou阈值 float iouThreshold = 0.45; std::vector<int> indices; //非极大值抑制算法 cv::dnn::NMSBoxes(boxes, confs, confThreshold, iouThreshold, indices); //定义box方框颜色表Color Scalar Color[] = { Scalar(0,0,255), //红色 hat Scalar(255,0,0), //蓝色 person }; //定义标签颜色表label Scalar Color_label[] = { Scalar(0,255,0), //绿色 hat Scalar(255,0,255), //粉色 person }; //判断是否未检测到目标 if (indices.empty()) { //未检测到目标返回value为-3 yolo_detection.value = -3; return yolo_detection; } //图像长度和宽度 int height = srcimg.rows, width = srcimg.cols; for (int i = 0; i < indices.size(); ++i) { //目标物体索引值index, classIds[index]为类别标签索引 int index = indices[i]; //置信度分数,保留两位小数 float scores = round(confs[index] * 100) / 100; std::ostringstream oss; oss << scores; //输出字符串留 cv::rectangle(srcimg, cv::Point(boxes[index].tl().x, boxes[index].tl().y), cv::Point(boxes[index].br().x, boxes[index].br().y), Color[classIds[index]], 2); cv::putText(srcimg, class_names[classIds[index]], Point(boxes[index].tl().x, boxes[index].tl().y - 5), FONT_HERSHEY_SIMPLEX, 0.5, Color_label[classIds[index]], 2); } //cv::imshow("pred_image", srcimg); //cv::imwrite("pred_image.jpg", srcimg); //cv::waitKey(0); //图像类型从BGR转换为RGB cv::cvtColor(srcimg, srcimg, cv::COLOR_BGR2RGB); //检测成功value为1 yolo_detection.value = 1; //申请内存空间 uchar* pred_image = (uchar*)malloc(sizeof(uchar) * height * width * 3); //内存拷贝 memcpy(pred_image, srcimg.data, height * width * 3); //buffer为输出 //预测结果图像 yolo_detection.pred_image = pred_image; //图像的高度和宽度 yolo_detection.height = height; yolo_detection.weight = width; return yolo_detection; } //重设大小函数 Mat resize_image(cv::Mat srcimg, int* newh, int* neww, int* top, int* left) { int srch = srcimg.rows, srcw = srcimg.cols; int inpHeight = 640; int inpWidth = 640; *newh = inpHeight; *neww = 640; bool keep_ratio = true; Mat dstimg; double height_scale = 1.0 *inpHeight / srcw, weight_scale = 1.0* inpWidth / srch; //后期box还原 r = min(height_scale, weight_scale); //取最小值 if (keep_ratio && srch != srcw) { float hw_scale = (float)srch / srcw; if (hw_scale > 1) { *newh = inpHeight; *neww = int(inpWidth / hw_scale); resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA); *left = int((inpWidth - *neww) * 0.5); copyMakeBorder(dstimg, dstimg, 0, 0, *left, inpWidth - *neww - *left, BORDER_CONSTANT, 114); } else { *newh = (int)inpHeight * hw_scale; *neww = inpWidth; resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA); *top = (int)(inpHeight - *newh) * 0.5; copyMakeBorder(dstimg, dstimg, *top, inpHeight - *newh - *top, 0, 0, BORDER_CONSTANT, 114); } } else { resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA); } return dstimg; }
在上述代码中,集成了onnx模型加载,检测框架推理,绘制方框等各环节。其中,resize_image函数的作用是将输入图像重设为640×640大小像素。
推理过程中设置置信度阈值为0.65,iou阈值为0.45,在绘制后将图像从BGR转换为RGB,这样方便后续在Matplotlib上可视化显示。由于需要返回预测图像至Python中,因此需要开辟内存空间,使用内存拷贝将cv::Mat类型转换为uchar*类型。头文件和源代码文件编写完成后,为了检测程序中是否有误,在‘生成’选项中选中‘编译’,实现对程序整体进行编译,编译正常不报错表明程序无问题。
(3)生成.dll可执行文件
在Visual Studio中,需要对动态链接库项目进行.dll可执行文件的生成,生成的方式如下图,可采用两种方式来生成:
方法①:在‘生成’选项中选中‘生成解决方案’以生成可执行文件,当程序代码更改后需要‘重新生成解决方案’才能生效。
方法②:生成.dll可执行文件还可以在VS右侧资源管理器中单机右键项目,选中‘生成’或者‘重新生成’,即可生成.dll文件。生成的.dll文件名称为
detection.dll
,项目名称不同,生成的文件名也不同,有关detection.dll
文件的分享下载链接在后文第三节(4)中给出。生成的文件路径为:- detection.dll文件路径:detection\x64\Release\detection.dll
注意:动态链接库的项目不可以进行调试和运行,只能用于.dll可执行文件生成,否则会报如下错误:
无法启动程序 ;
程序不是有效的Win32应用程序。
三、Python加载调用动态链接库
本节主要介绍了对动态链接库进行调用,在Win11和Win7系统上编写Python程序实现对动态链接库文件的正常调用。
(1)Python加载调用
编写Python程序实现对动态链接库的加载调用,返回结果图像用
Matplotlib
可视化,无需调用OpenCV包。
Matplotlib
是一个强大的可视化图像的库,可以绘画出各种美观好看的图,这里我们仅用它来做可视化操作。编写加载调用动态链接库的Python程序onnx_detection_dll.py,该程序在Win7电脑上配置好环境后也可以运行,代码如下:import ctypes,os import numpy as np import matplotlib.pyplot as plt #定义返回类型 class MyStruct(ctypes.Structure): _fields_ = [('value', ctypes.c_int), ('pred_image', ctypes.POINTER(ctypes.c_ubyte)),('height', ctypes.c_int),('weight', ctypes.c_int)] #Dll路径 DLL_path = "detection.dll" #图像路径 image_path = "Safety_Helmet\\JPEGImages\\000000.jpg" # 图像路径string转换为C++可以读取的 const char* image_path_char = (bytes(image_path, 'utf-8')) #onnx模型路径 onnx_path = "best.onnx" # const wchar_t*类型不用进行转换 detection_dll = ctypes.cdll.LoadLibrary(DLL_path) #加载DLL文件 #定义动态链接库dll的返回值,返回值为结构体类型,结构体中的各个参数(判断检测是否正常,返回检测结果图像) detection_dll.object_detection.restype = MyStruct # 定义动态链接库dll传送参数,包括图像路径和模型路径 params_1 = { "param1": ctypes.c_char_p(image_path_char), "param2": ctypes.c_wchar_p(onnx_path), } # 调用目标检测dll,传递参数 result = detection_dll.object_detection(params_1["param1"], params_1["param2"]) #判断安全帽检测是否成功 value = result.value #返回值seg_exist为-1表示模型加载失败,-2表示图像读取失败,-3表示模型未检测到 if value!=1: if value==-1: print("模型加载失败") elif value==-2: print("图像读取失败") elif value==-3: print("未检测到物体") else: print("其它情况") if value==1: row = result.height #图像高度 col = result.weight #图像宽度 # 检测结果预测图像 pred_image = result.pred_image #类型转换为array并重设大小 pred_image = np.array(np.fromiter(pred_image, dtype=np.uint8, count=row*col*3)) pred_image = pred_image.reshape((row,col,3)) #图像转换为array #展示图像 plt.imshow(pred_image) plt.show()
Python调用代码编写较为简单,该代码所包含的输入和输出变量参数整理如下表格所示,输入包含三个参数,输出两个参数,输出的参数value值为1时表明检测成功,其它值时表明检测有问题;pred_image为输出检测后的预测图像。
参数 变量名 赋值 含义 输入 DLL_path "detection.dll"
动态链接库文件路径 image_path "000000.jpg" 图像文件路径 onnx_path "best.onnx" .onnx模型文件路径 输出 value 1 检测成功 -1 模型加载失败 -2 图像读取失败 -3 未检测到目标 其它值 其它情况 pred_image Numpy类型矩阵 预测图像 注意:执行该程序前需要把opencv_world412.dll文件放入操作系统的系统文件夹System32下,具体文件路径和操作系统文件夹路径如下:
- opencv_world412.dll文件路径:opencv\build\x64\vc15\bin\opencv_world412.dll
- 系统文件夹路径:C:\Windows\System32\
这里附上该文件的下载链接,需要的可以自取:
- opencv_world412.dll文件
- 链接:https://wwf.lanzouj.com/iUeB91wmfsod
- 密码:czjb
若在系统环境变量中缺失该文件,会导致程序报如下两种类型的错误:
①由于找不到opencv_world412.dll,无法继续执行代码。重新安装程序可能会解决此问题。
②FileNotFoundError: Could not find module ‘detection.dll’ (or one of its dependencies). Try using the full path with constructor syntax.
(2)Win7虚拟机安装
采用虚拟机VMware17安装Win7系统,首先下载VMware17软件,下载链接:
VMware17下载: http://www.xz7.com/downinfo/590026.html
使用迅雷下载系统IOS镜像地址,该地址的操作系统中包含许多操作系统,这里只下载Win7旗舰版的镜像:
cn_windows_7_ultimate_x64_dvd_x15-66043.iso
下载ios镜像: https://msdn.itellyou.cn/
之后启动VMware17进行安装,在VMware17上按步骤操作来安装虚拟环境:
参考CSDN: https://blog.csdn.net/u011029104/article/details/131502315
这里给虚拟环境起名为test_dll,设置CPU数量为1,内核数为4,内存为8GB,硬盘空间设置100~200GB,之后Win7系统的虚拟环境就成功创建。
(3)虚拟机环境配置
虚拟环境安装好后,首先需要在在Win7系统属性中开启允许其它计算机远程连接这台计算机,如下图所示:
往虚拟环境传送文件可以用VMware Tools工具,这里使用IPv4地址远程桌面连接进行文件传输,需要在cmd终端使用ipconfig指令查看IPv4地址,如下图所示。这里的IPv4地址为192.168.72.129,之后即可启用远程桌面连接,用户名为test_dll,密码为创建环境时自己设定的。同时为了方便,可以在虚拟机中对计算机管理中的磁盘管理进行压缩卷分区。
之后,在Win7上下载安装Anaconda,Win7上安装清华镜像Anaconda3包的版本为Anaconda3-2019.10-Windows-
x86_64.exe,清华镜像的官方下载链接为:
Anaconda3-2019.10-Windows-x86_64.exe
点击链接即可直接下载。Anaconda3下载后在Win7系统上按照步骤安装即可,安装过程如果遇到如下图所示的报错,则表明可能缺少了部分安装所需的动态链接库文件,需要将这些动态链接库.dll文件放到系统文件夹System32下面,这些.dll文件的下载链接如下:
- 动态链接库dll文件
- 链接:https://wwf.lanzouj.com/iuxlt1u80ouj
- 密码:erky
(4)Win7加载调用
在Win7上拷贝测试的图像、权重模型best.onnx、动态链接库
detection.dll
、测试程序onnx_detection_dll.py,这里整理好在Win7上拷贝所需的各个文件,在Win10和Win11上同样也可以运行:- 权重模型、动态链接库、程序和待测图像
- 链接:https://wwf.lanzouj.com/irIta1xvlyja
- 密码:6h74
在cmd终端运行onnx_detection_dll.py程序,可得出
Matplotlib
可视化安全帽检测结果,如下图。
注意:若程序报错的话可能是系统文件夹中缺少相关的.dll文件,需要在Win7的系统文件夹System32中放置所需的.dll可执行文件,缺少这些文件均会导致程序报错,具体在Win7上的程序报错说明和解决方案见第四节(2)。
四、报错原因说明
(1)Win11上错误
① 找不到opencv_world412.dll
缺少opencv_world412.dll文件,可能会报以下两种错误:
1. 由于找不到opencv_world412.dll,无法继续执行代码。重新安装程序可能会解决此问题。
2. FileNotFoundError: Could not find module ‘detection.dll’ (or one of its dependencies). Try using the full path with constructor syntax.以上两个错误的原因都是因为系统文件夹中缺少
opencv_world412.dll
文件,该文件的分享链接见本节(2)①中。② 未在VS中添加附加依赖项onnxruntime.lib
在VS中生成动态链接库dll时,如果未在属性配置中添加附加依赖项onnxruntime.lib,则可能会报以下错误:
无法解析的外部符号 OrtGetApiBase;1 个无法解析的外部命令该错误的解决方法需要在Visual Studio(VS)项目的配置属性中,链接器选项下的输入中,在附加依赖项中添加
onnxruntime.lib
,如下图所示:
③ VS中onnxruntime包编译报错生成动态链接库时报错:class “OrtApi” 没有成员……
……不是 “OrtApi” 的成员
出现该错误的原因是由于在属性配置中onnxruntime包的版本用的不对,使用1.6.0版本的onnxruntime库会出现该类问题,使用1.5.1版本的onnxruntime即可。(2)Win7上错误
① 丢失opencv_world412.dll
无法启动此程序,因为计算机中丢失opencv_world412.dll。尝试重新安装该程序以解决此问题。
出现该错误的原因是由于在系统文件夹System32中缺失了opencv_world412.dll文件,需要在系统文件夹C:\Windows\System32下补充添加该文件。opencv_world412.dll的下载链接:- opencv_world412.dll文件
- 链接:https://wwf.lanzouj.com/iUeB91wmfsod
- 密码:czjb
② 丢失onnxruntime.dll
无法启动此程序,因为计算机中丢失onnxruntime.dll。尝试重新安装该程序以解决此问题。
出现该错误的原因是由于在系统文件夹System32中缺失了onnxruntime.dll文件,需要在系统文件夹C:\Windows\System32下补充添加该文件。onnxruntime.dll的下载链接:- onnxruntime.dll文件(1.5.1 版本)
- 链接:https://wwf.lanzouj.com/iSvcD1xwrnna
- 密码:d5v1
③ 丢失VCRUNTIME140_1.dll
无法启动此程序,因为计算机中丢失VCRUNTIME140_1.dll。尝试重新安装该程序以解决此问题。
出现该错误的原因是由于在系统文件夹System32中缺失了vcruntime140_1.dll文件,需要在系统文件夹C:\Windows\System32下补充添加该文件。vcruntime140_1.dll的下载链接:- vcruntime140_1.dll文件
- 链接:https://wwf.lanzouj.com/irqGA1y3xl7a
- 密码:c96i
④ 内存位置访问无效
OSError:[WinError 998] 内存位置访问无效。
导致该错误的原因是由于系统文件夹下的onnxruntime.dll文件版本过低,推荐使用onnxruntime.dll为1.5.1版本。
使用过高的onnxruntime.dll版本文件的话需要在系统文件夹下放置其它相关的.dll文件,比如1.8.0及其以上的版本。⑤ exception:access violation reading
OSError: exception: access violation reading 0x0000000000000018
出现该错误的原因是因为动态链接库编译生成的.dll版本过高,需要在属性配置中修改加载读取onnxruntime库的版本,如下图所示,分别在附加库目录和附加包含目录中添加onnxruntime库下的include和lib路径。
附件:Win11安装包下载链接
本节整理在Win11上环境配置时有关安装包的下载链接,包括Anaconda、显卡加速库、深度学习库和其它安装包。
(1)Anaconda3
Anaconda3所用版本为 Anaconda3-2021.04-Windows-x86_64,下载链接为:
Anaconda3-2021.04-Windows-x86_64.exe
点击链接即可直接下载。(2)CUDA 和 cuDNN
CUDA使用版本为 11.1.0,下载链接为:
cuda_11.1.0_456.43_win10
点击链接即可直接下载。CUDA其它历史版本的下载官网为:
https://developer.nvidia.com/cuda-toolkit-archivecuDNN使用版本为 8.0.4,由于下载cuDNN需要登陆,仅给出cuDNN历史版本的下载官网:
https://developer.nvidia.com/rdp/cudnn-archive(3)PyTorch 和 TorchVision
PyTorch使用版本为 1.8.0,下载链接为:
pytorch-1.8.0-py3.8_cuda11.1_cudnn8_0.tar.bz2
点击链接即可直接下载。TorchVision使用版本为 0.9.0,下载链接为:
torchvision-0.9.0-py38_cu111.tar.bz2
点击链接即可直接下载。注意:PyTorch 和 TorchVision 下载成功后需要在终端用 conda install 指令来安装以上安装包。
(4)其它安装包
① OpenCV-Python
OpenCV-Python使用版本为 4.1.2.30,下载链接为:
opencv_python-4.1.2.30-cp38-cp38-win_amd64.whl
点击链接即可直接下载。② Numpy
Numpy版本过低的话在后面安装其它包时可能需要升级至高版本,Numpy使用版本为 1.21.0,下载链接为:
numpy-1.21.0-cp38-cp38-win_amd64.whl
点击链接即可直接下载。③ protobuf
protobuf版本为 3.16.0,下载链接为:
protobuf-3.16.0-py2.py3-none-any.whl
点击链接即可直接下载。④ Onnx
Onnx版本为 1.12.0,下载链接为:
onnx-1.12.0-cp38-cp38-win_amd64.whl
点击链接即可直接下载。⑤ ONNX Runtime
OnnxRuntime版本为 1.12.0,下载链接为:
onnxruntime-1.12.0-cp38-cp38-win_amd64.whl
点击链接即可直接下载。⑥ TensorBoard
TensorBoard版本为 2.4.1,下载链接为:
tensorboard-2.4.1-py3-none-any.whl
点击链接即可直接下载。⑦ grpcio
grpcio版本为 1.26.0,下载链接为:
grpcio-1.26.0-cp38-cp38-win_amd64.whl
点击链接即可直接下载。⑧ GitPython
执行yolo训练程序train.py时如果缺少GitPython包,程序有可能会报以下错误:
ModuleNotFoundError: No module named ‘git’GitPython版本为 3.0.0,下载链接为:
GitPython-3.0.0-py3-none-any.whl
点击链接即可直接下载。⑨ gitdb2
缺少gitdb2包在执行训练程序时可能会报以下错误:
ModuleNotFoundError: No module named ‘gitdb.utils.compat’gitdb2版本为 3.0.1,下载链接为:
gitdb2-3.0.1-py2.py3-none-any.whl
点击链接即可直接下载。安装以上包也可以使用清华镜像来安装,比如清华镜像安装 tensorboard 2.4.1:
pip install tensorboard==2.4.1 -i https://pypi.tuna.tsinghua.edu.cn/simple注意:以上安装包下载成功后需要在终端用 pip install 指令来安装以上安装包。