前言

​ 本文提供了将Python算法嵌入C++C语言的两种实现思路。算法大多是由Python语言编写,而我们开发软件大多使用的还是C++,比如常见的QTC#等,两种不同的语言之间如何实现通信呢?有的将算法打包为.exe文件,通过软件去启动这个.exe,这种方法并不优雅。我们知道Python的底层是C语言编写,叫CPython,本文方法便是通过CPython调用Python脚本,实现数据交互。

​ 假设我们现在使用QT开发软件界面,通过摄像头去识别物体,所用算法为YoloV5,将带有检测结果的视频显示在QT界面上。

方法一

QT开启一个视频接收的线程,通过opencv接收,将每一帧图像存放在消息队列中,假设消息队列叫srcMatQueue;再开启一个图像检测线程,将图像从srcMatQueue中取出,将图像传递给Python算法,获取返回之后的图像,并存入消息队列detectMatQueue中;再主线程(GUI)中,取出detectMatQueue中的图像,转为QImage格式,然后显示即可。

​ 这里重点介绍第二个图像检测线程。首先,要调用CPython,我们需要包含其头文件Python.h,如下

1
2
3
4
5
6
#include <python/Python.h>
#include <numpy/ndarrayobject.h>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>

调用Python算法可分为一下几步:

  1. 初始化Python环境
  2. 加载Python模块
  3. 获取模块中的函数名、类名等
  4. 获取类名
  5. 实例化类
  6. 调用类中的方法

显然,我们需要将Python算法封装为一个类然后调用,(不封装也可以,只是封装为类后,我们可以在初始化时先加载模型,这样可以节省检测时多次加载模型),以YoloV5为例,我们只要将detect.py文件重新封装一下即可。我们封装的函数主要有三个,分别是__init__loadModeldetect__init__是实例化这个类时执行的函数,主要对一些变量进行初始化;loadModel函数是单独加载模型文件的函数,一般来说,加载模型比较耗时,你也可以直接将其写在初始化函数里;detect函数是对QT传给它的图像进行处理的函数,所以需要有一个入口参数frame,算法对frame进行处理后将结果return即可,需要注意的是返回的类型,QT端对返回值进行解析,得到处理后的图片和检测结果。

​ 以下是YoloV5detect.py封装示例。

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
# This Python file uses the following encoding: utf-8

import os
import sys
from pathlib import Path

import cv2
import torch
# import torch.backends.cudnn as cudnn
import numpy as np
FILE = Path(__file__).resolve()
ROOT = FILE.parents[0] # YOLOv5 root directory
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
from utils.augmentations import letterbox
from models.common import DetectMultiBackend
from utils.datasets import IMG_FORMATS, VID_FORMATS
from utils.general import (LOGGER, non_max_suppression, scale_coords)
from utils.plots import Annotator, colors
from utils.torch_utils import select_device, time_sync


class YoloV5:
def __init__(self) -> None:
self.img_size = 640
self.stride = 32
self.weights = ROOT / 'yolov5s.pt'
self.data = ROOT / 'data/coco128.yaml'
self.device = 0
self.conf_thres = 0.25
self.iou_thres = 0.45
self.max_det = 1000
self.dnn = False
self.imgsz = (640,640)
self.augment=False
self.visualize=False
self.classes=None
self.agnostic_nms=False
self.line_thickness=3
self.hide_labels=False # hide labels
self.hide_conf=True # hide confidences
self.view_img=False

def load_img(self, img0):
# Padded resize
img = letterbox(img0, self.img_size, stride=self.stride, auto=True)[0]
# Convert
img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
img = np.ascontiguousarray(img)
return img

def select_dev(self, dev):
self.device = dev

def load_model(self):
self.dev = select_device(self.device)
self.model = DetectMultiBackend(self.weights, self.dev, self.dnn, self.data)

def detect(self, srcImg):

names = self.model.names
# imgsz = check_img_size(imgsz, s=stride) # check image size

im = self.load_img(srcImg)
im = torch.from_numpy(im).to(self.dev)
im = im.float() # uint8 to fp16/32
im /= 255
if len(im.shape) == 3:
im = im[None]
pred = self.model(im, augment=self.augment, visualize=self.visualize)
# NMS
pred = non_max_suppression(pred, self.conf_thres, self.iou_thres, self.classes, self.agnostic_nms, max_det=self.max_det)
# Process predictions
det = pred[0]
im0 = srcImg.copy()
im0rect = [0,0,0,0]
# roi_rect = RECT()
# f(((0, 0), (400, 300)), (10, 10))
isexsit = 0
annotator = Annotator(im0, line_width=self.line_thickness, example=str(names))
if len(det):
# Rescale boxes from img_size to im0 size
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()
for *xyxy, conf, cls in reversed(det):
c = int(cls) # integer class
label = None if self.hide_labels else (names[c] if self.hide_conf else f'{names[c]} {conf:.2f}')
if label == 'person':
isexsit = 1
annotator.box_label(xyxy, label, color=colors(c, True))
im0rect[0] = (int(xyxy[0]))
im0rect[1] = (int(xyxy[1]))
im0rect[2] = (int(xyxy[2]))
im0rect[3] = (int(xyxy[3]))

im0 = annotator.result()

if self.view_img:
cv2.imshow("img", im0)
cv2.waitKey(0) # 1 millisecond

return im0.copy(),isexsit,im0rect[0],im0rect[1],im0rect[2],im0rect[3]



if __name__ == '__main__':
yolo = YoloV5()
yolo.load_model()
img = cv2.imread(str(ROOT / 'data/images/bus.jpg'))
im0, nd = yolo.detect(img)
cv2.imshow("img", im0)
key = cv2.waitKey(0)
if key == ord('q'):
cv2.destroyAllWindows()

这种方法我已经在QT端进行了实现,需要说明的是如果算法检测速度够快,第二个线程不要也行,直接在GUI线程进行调用也是可以的。

方法二

​ 这种方法未进行实现,理论上可行。即 将视频接收与处理都在Python端完成,QT端只进行对Python脚本的开启与处理后的图像接收即可。具体来说,QT启动Python脚本,Python将处理后的图像和数据存放在变量中,QT定时查询变量即可。

链接

演示视频:点击
开源仓库:点击