上篇文章演示了使用飞凌OK3568-C开发板外接USB摄像头进行AI物品识别,并进行了视频演示,本篇来介绍下代码实现。
1 SSD模型介绍
SSD,全称为Single Shot MultiBox Detector,是Wei Liu在ECCV 2016上提出的一种目标检测算法,属于一阶段One Stage方法,SSD 模型利用不同尺度的特征图进行目标的检测,其模型结构图如下:
SSD具有如下主要特点:
- 从YOLO中继承了将detection转化为regression的思路,同时一次即可完成网络训练
- 基于Faster RCNN中的anchor,提出了相似的prior box
- 加入基于特征金字塔(Pyramidal Feature Hierarchy)的检测方式,相当于半个FPN思路
SSD网络结构图如下:
其算法步骤为:
- 将图像输入预训练好的分类网络(基于VGG16-Atrous)得到不同大小的特征映射
- 分别提取Conv4_3、Conv7、Conv8_2、Conv9_2、Conv10_2、Conv11_2层的特征映射feature map,在每个特征映射的每个点构造6个不同大小尺度的bounding box,进行检测和分类来生成一些列bounding box
- 采用NMS处理不同特征映射的bounding box,删掉部分重叠或者不正确的bounding box,得到最终的检测框
OK3568-C开发板中自带了已训练好的AI模型,位于/userdata/model目录下的ssd_inception_v2.rknn,我们直接用就可以了。
2 USB摄像头实现物品识别代码
先来看下整个代码的项目结构,然后再来分别介绍各个功能模块。
- imageutil.h:图像类型转换相关函数
- myvideosourceface.cpp/h:用于USB摄像头图像显示
- qtcamera.cpp/h:qt界面
- rknn_ssd_process.cpp/h:用于SSD模型进行AI物品识别的接口函数
- rknn_ssd.cpp/h:SSD模型相关函数
3 按帧获取USB摄像头图像
3.1 改为自己的Viewfinder
之前测试USB摄像头显示时,使用的是Qt的QCameraViewfinder用来显示摄像头图像,为了能获取到每一帧的图像,可以自己实现一个Viewfinder,然后在m_camera->setViewfinder时设置为自己的,并添加槽函数rcvFrame,当获取到一帧图像时,会触发此函数。
void qtCamera::on_cameraClick()
{
m_camera = new QCamera(m_cameraInfo);
m_camera->unload();
m_camera->setCaptureMode(QCamera::CaptureStillImage);
QCameraViewfinderSettings set;
set.setResolution(640, 480);
set.setMaximumFrameRate(25);
myvideosurface *surface = new myvideosurface(this);
m_camera->setViewfinder(surface);
connect(surface, SIGNAL(frameAvailable(QVideoFrame)), this, SLOT(rcvFrame(QVideoFrame)), Qt::DirectConnection);
connect(this,SIGNAL(sendOneQImage(QImage)), this, SLOT(recvOneQImage(QImage)));
m_camera->start();
}
接收到一帧图像后,其原始图像格式是QVideoFrame类型的,需要先转为QImage类型,然后就可以进行显示或进行图像处理了,这里触发一个sendOneQImage信号来通知进行图像处理:
void qtCamera::rcvFrame(QVideoFrame m_currentFrame)
{
m_currentFrame.map(QAbstractVideoBuffer::ReadOnly);
QImage videoImg = QImage(m_currentFrame.bits(),
m_currentFrame.width(),
m_currentFrame.height(),
QVideoFrame::imageFormatFromPixelFormat(m_currentFrame.pixelFormat())).copy();
m_currentFrame.unmap();
QWidget::update();
emit sendOneQImage(videoImg);
}
3.2 自定义Viewfinder的实现
参考网上的一些代码实现,其主要逻辑如下:
bool myvideosurface::present(const QVideoFrame &frame)
{
if (frame.isValid())
{
QVideoFrame cloneFrame(frame);
emit frameAvailable(cloneFrame);
return true;
}
stop();
return false;
}
bool myvideosurface::start(const QVideoSurfaceFormat &videoformat)
{
if(QVideoFrame::imageFormatFromPixelFormat(videoformat.pixelFormat()) != QImage::Format_Invalid && !videoformat.frameSize().isEmpty())
{
QAbstractVideoSurface::start(videoformat);
return true;
}
return false;
}
void myvideosurface::stop()
{
QAbstractVideoSurface::stop();
}
bool myvideosurface::isFormatSupported(const QVideoSurfaceFormat &videoformat) const
{
return QVideoFrame::imageFormatFromPixelFormat(videoformat.pixelFormat()) != QImage::Format_Invalid;
}
QList<QVideoFrame::PixelFormat> myvideosurface::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const
{
if(handleType == QAbstractVideoBuffer::NoHandle){
return QList<QVideoFrame::PixelFormat>() << QVideoFrame::Format_RGB32
<< QVideoFrame::Format_ARGB32
<< QVideoFrame::Format_ARGB32_Premultiplied
<< QVideoFrame::Format_RGB565
<< QVideoFrame::Format_RGB555;
qDebug() << QList<QVideoFrame::PixelFormat>() << QVideoFrame::Format_RGB32;
}
else
{
return QList<QVideoFrame::PixelFormat>();
}
}
对应的头文件类定义:
class myvideosurface : public QAbstractVideoSurface
{
Q_OBJECT
public:
explicit myvideosurface(QObject *parent = nullptr);
~myvideosurface() Q_DECL_OVERRIDE;
bool present(const QVideoFrame &) Q_DECL_OVERRIDE;
bool start(const QVideoSurfaceFormat &) Q_DECL_OVERRIDE;
void stop() Q_DECL_OVERRIDE;
bool isFormatSupported(const QVideoSurfaceFormat &) const Q_DECL_OVERRIDE;
QList<QVideoFrame::PixelFormat> supportedPixelFormats(QAbstractVideoBuffer::HandleType type = QAbstractVideoBuffer::NoHandle) const Q_DECL_OVERRIDE;
private:
QVideoFrame m_currentFrame;
signals:
void frameAvailable(QVideoFrame);
};
4 图像类型的转换与显示
4.1 QImage转Mat
Qt是QCamera创建的USB摄像头,获取到的图片格式是QImage类型,而使用OpenCV进行图像处理,需要转换为cv::Mat类型,转换的方式如下:
cv::Mat QImageToMat(QImage image)
{
image = image.convertToFormat(QImage::Format_RGB888);
cv::Mat tmp(image.height(), image.width(), CV_8UC3, (uchar *)image.bits(), image.bytesPerLine());
cv::Mat result; // deep copy just in case (my lack of knowledge with open cv)
cvtColor(tmp, result, CV_BGR2RGB);
return result;
}
4.2 Mat转QImage
OpenCV进行图像处理完成后,比如进行AI物品识别完成,并将识别的信息标记到图像上后,需要再转成QImage的类型用于在Qt中显示出来,转换的方式如下:
QImage MatToQImage(cv::Mat mat)
{
cv::cvtColor(mat, mat, CV_BGR2RGB);
QImage qim((const unsigned char *)mat.data, mat.cols, mat.rows, mat.step,
QImage::Format_RGB888);
return qim;
}
4.3 QImage转QPixmap
QImage在Qt中还不能直接显示出来,还需要再转为QPixmap类型,转换的方式如下:
QImage qImage;
QPixmap tempPixmap = QPixmap::fromImage(qImage);
4.4 图像的显示
这里创建一个QLabel用于显示图像,调用setPixmap方法即可将图像显示出来,最后的adjustSize用来自动调整大小。
m_lableShowImg = new QLabel();
m_lableShowImg->setPixmap(tempPixmap);
m_lableShowImg->adjustSize();
5 RKNN例程移植
飞凌OK3568-C开发板资料中,自带了ssd模型的测试程序,代码位置如下,ssd的测试代码是这3个文件:
测试代码,需要在执行时,输入模型的目录位置和测试图片的位置,AI物品识别之后会产生一个输出图片,需要再使用图片查看器查看结果。
为了方便功能的调用,这里将fltest_opencv_rknn_ssd_main.cc改写为rknn_ssd_process.cpp,并将具体功能进行拆分,封装为C++的形式。
5.1 按功能封装为C++形式
自己封装的RknnSsdModel类定义:
class RknnSsdModel
{
public:
RknnSsdModel(){};
~RknnSsdModel(){};
int RknnInit(const char *model_path);
int RknnDeInit();
unsigned char *LoadModel(const char *filename, int *model_size);
int DoRknnSsd(cv::Mat &src, cv::Mat &res);
private:
unsigned char *m_pModel = nullptr;
rknn_context m_rknnCtx;
rknn_input_output_num m_rknnIoNum;
};
5.1.1 RKNN初始化
主要功能是根据传入的rknn模型进行相关的初始化
int RknnSsdModel::RknnInit(const char *model_path)
{
int ret = 0;
int model_len = 0;
printf("Loading model ...\n");
m_pModel = LoadModel(model_path, &model_len);
printf("rknn_init ...\n");
ret = rknn_init(&m_rknnCtx, m_pModel, model_len, 0, NULL);
if (ret < 0)
{
printf("rknn_init fail! ret=%d\n", ret);
return -1;
}
ret = rknn_query(m_rknnCtx, RKNN_QUERY_IN_OUT_NUM, &m_rknnIoNum, sizeof(m_rknnIoNum));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printf("model input num: %d, output num: %d\n", m_rknnIoNum.n_input, m_rknnIoNum.n_output);
printf("input tensors:\n");
rknn_tensor_attr input_attrs[m_rknnIoNum.n_input];
memset(input_attrs, 0, sizeof(input_attrs));
for (int i = 0; i < m_rknnIoNum.n_input; i++)
{
input_attrs[i].index = i;
ret = rknn_query(m_rknnCtx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printRKNNTensor(&(input_attrs[i]));
}
printf("output tensors:\n");
rknn_tensor_attr output_attrs[m_rknnIoNum.n_output];
memset(output_attrs, 0, sizeof(output_attrs));
for (int i = 0; i < m_rknnIoNum.n_output; i++)
{
output_attrs[i].index = i;
ret = rknn_query(m_rknnCtx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printRKNNTensor(&(output_attrs[i]));
}
return ret;
}
5.1.2 RKNN运行
传入一张Mat格式的图片(一帧视频图像),经过AI识别,并将识别的信息标注到图片上后,将识别结果也以Mat格式传出:
int RknnSsdModel::DoRknnSsd(cv::Mat &src, cv::Mat &res)
{
const int img_width = 300;
const int img_height = 300;
const int img_channels = 3;
int ret = 0;
cv::Mat img = src.clone();
if (src.cols != img_width || src.rows != img_height)
{
printf("resize %d %d to %d %d\n", src.cols, src.rows, img_width, img_height);
cv::resize(src, img, cv::Size(img_width, img_height), (0, 0), (0, 0), cv::INTER_LINEAR);
}
rknn_input inputs[1];
memset(inputs, 0, sizeof(inputs));
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8;
inputs[0].size = img.cols * img.rows * img.channels();
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].buf = img.data;
ret = rknn_inputs_set(m_rknnCtx, m_rknnIoNum.n_input, inputs);
if (ret < 0)
{
printf("rknn_input_set fail! ret=%d\n", ret);
return -1;
}
printf("rknn_run\n");
ret = rknn_run(m_rknnCtx, nullptr);
if (ret < 0)
{
printf("rknn_run fail! ret=%d\n", ret);
return -1;
}
rknn_output outputs[2];
memset(outputs, 0, sizeof(outputs));
outputs[0].want_float = 1;
outputs[1].want_float = 1;
ret = rknn_outputs_get(m_rknnCtx, m_rknnIoNum.n_output, outputs, NULL);
if (ret < 0)
{
printf("rknn_outputs_get fail! ret=%d\n", ret);
return -1;
}
detect_result_group_t detect_result_group;
postProcessSSD((float *)(outputs[0].buf), (float *)(outputs[1].buf), src.cols, src.rows, &detect_result_group);
rknn_outputs_release(m_rknnCtx, 2, outputs);
for (int i = 0; i < detect_result_group.count; i++)
{
detect_result_t *det_result = &(detect_result_group.results[i]);
printf("%s @ (%d %d %d %d) %f\n",
det_result->name,
det_result->box.left, det_result->box.top, det_result->box.right, det_result->box.bottom,
det_result->prop);
int x1 = det_result->box.left;
int y1 = det_result->box.top;
int x2 = det_result->box.right;
int y2 = det_result->box.bottom;
rectangle(src, Point(x1, y1), Point(x2, y2), Scalar(255, 0, 0, 255), 3);
putText(src, det_result->name, Point(x1, y1 - 12), 1, 4, Scalar(0, 255, 0, 255), 4);
}
res = src;
return 0;
}
5.2 AI识别调用
OK3568-C开发板中自带了已训练好的AI模型,位于/userdata/model目录下的ssd_inception_v2.rknn,在程序初始化时需要用到。
** AI识别的代码逻辑为:先在qtCamera初始化时调用RKNN的初始化,然后打开USB摄像头,USB获取到每帧图像后, 调用DoRknnSsd进行AI物品识别,最后将识别的结果通过setPixmap方法展示出来**
std::string ssd_model = "/userdata/model/ssd_inception_v2.rknn";
m_rknnModel.RknnInit(ssd_model.c_str());
void qtCamera::recvOneQImage(QImage qImage)
{
cv::Mat srcImg = ImageUtil::QImageToMat(qImage);
cv::Mat dstImg;
m_rknnModel.DoRknnSsd(srcImg, dstImg);
QImage qDstImage = ImageUtil::MatToQImage(dstImg);
QPixmap tempPixmap = QPixmap::fromImage(qDstImage);
m_lableShowImg->setPixmap(tempPixmap);
m_lableShowImg->adjustSize();
}
5.3 编译
需要注意下Qt工程的配置文件,要把opencv的一些库链接进去
qcamera.pri
INCLUDEPATH += $$PWD/src
HEADERS += \
$$PWD/src/qtcamera.h \
$$PWD/src/myvideosurface.h \
$$PWD/src/rknn_ssd.h \
$$PWD/src/rknn_ssd_process.h \
$$PWD/src/imageutil.h
SOURCES += \
$$PWD/src/qtcamera.cpp \
$$PWD/src/myvideosurface.cpp \
$$PWD/src/rknn_ssd.cpp \
$$PWD/src/rknn_ssd_process.cpp
qcamera.pro
TARGET = USBCameraSSD
TEMPLATE = app
QT += widgets multimedia multimediawidgets
SOURCES += main.cpp
include($$PWD/qcamera.pri)
LIBS+=-lopencv_core -lopencv_objdetect -lopencv_highgui -lopencv_videoio -lopencv_imgproc -lopencv_imgcodecs -lrknn_api -lOpenCL -lpthread
DESTDIR = $$PWD/app_bin
MOC_DIR = $$PWD/build/qcamera
OBJECTS_DIR = $$PWD/build/qcamera
最后的编译脚本还和之前的一样:
#! /bin/bash
mkdir -p build
cd build
export PATH=/home/xxpcb/myTest/OK3568/sourcecode/OK3568-linux-source/buildroot/output/OK3568/host/bin:$PATH
qmake .. && make
6 总结
本篇介绍了在飞凌OK3568-C开发板中,外接USB摄像头,利用Qt和RKNN进行AI物品识别,通过已训练好的SSD模型,进行摄像头画面的实时AI物品检查的代码实现原理。