关于C++&Python混编实现算法检测的两种方法

前言

​ 本文提供了将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,如下

#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封装示例。

# 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定时查询变量即可。

链接

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