打算在树莓派上挂载摄像头,通过WIFI模块传输到上位机。局域网内带宽不是问题,为了保证实时性,也没有必要进行复杂的视频编码和解码,于是通过截图然后使用UDP协议传输应该是可以的。所以最近试探性地使用了Qt和opencv进行测试,上位机接收到视频帧后使用Haar人脸识别后再传回一个坐标给下位机,结果还行。
1.下位机(图像采集端)
Qt中使用QUdpSocket类来发送和接收UDP数据报。Socket就是套接字,简单来说就是一个IP地址加上port端口号。它支持IPv4广播,简单起见这里使用广播模式。VideoCapture类用于获取视频或者摄像头设备。
private:
Ui::Sender *ui;
QUdpSocket *sender;
QUdpSocket *receiver;
cv::Mat frame;
int timerID;
cv::VideoCapture capture;
- 在.h文件中声明两个QUdpSocket和QVideoCapture成员变量
Sender::Sender(QWidget *parent) :
QDialog(parent),
ui(new Ui::Sender)
{
ui->setupUi(this);
sender = new QUdpSocket(this);
capture.open(0);
if(!capture.isOpened())
{
cout << "open failed" <<endl;
}
int delay =1000/10;
timerID = this->startTimer(delay);
receiver = new QUdpSocket(this);
receiver->bind(45455, QUdpSocket::ShareAddress);
connect(receiver, &QUdpSocket::readyRead, this, &Sender::processPendingDatagram);
}
VideoCapture对象使用open()方法来打开视频或者摄像头。传入int是指定设备ID,可以在设备管理器查找,一般是0。也可以传入路径来打开视频文件。startTimer方法是打开Qt的软件定时器,参数是毫秒,这里用来采集摄像头的图像。1000/10即10FPS。
为了方便处理,这里创建两个QUdpSocket对象,一个用于传输,一个用于接收,并分开两个端口。bind方法第一个参数是端口号,QUdpSocket::ShareAddress指允许其他服务器绑定到相同的地址和端口上。
每次有数据报到来时,QUdpSocket都会发射readyRead()信号。连接这个信号到自定义的槽中,便可进行读取操作。
void Sender::timerEvent( QTimerEvent *event)
{
if(event->timerId() == timerID)
{
if(capture.isOpened())
{
capture.read(frame);
}
cvtColor(frame,frame,CV_BGR2RGB); //BGRtoRGB
QImage image((unsigned char *)(frame.data),
frame.cols,frame.rows,
QImage::Format_RGB888);
ui->label->setPixmap(QPixmap::fromImage(image));
ui->label->resize(image.width(),image.height());
QByteArray byte;
//字节数组 要进行传输必须先转换成这个格式
QBuffer buff(&byte);
// 建立一个用于IO读写的缓冲区
image.save(&buff,"JPEG");
// image先向下转为byte的类型,再存入buff
QByteArray compressByte = qCompress(byte,1);
//数据压缩算法
QByteArray base64Byte = compressByte.toBase64();
//数据加密算法
sender->writeDatagram(base64Byte.data(),base64Byte.size(),
QHostAddress::Broadcast, 45454);
}
}
VideoCapture类使用read()方法来读取视频帧,存入一个Mat中。Mat存储图像默认是BGR编码,为了方便转换为Qimage格式进行操作,需要使用cvtColor改变编码模式。
Mat中的data()方法返回数据的一个uchar型的指针。使用这个指针,指定行数列数和编码模式,就可以构造一个Qimage对象,并在Qlabel中显示出来。
QBuffer类用于各种IO的读写。Qimage的save()方法将数据转为byte并存入一个QBuffer对象中。对byte对象进行简单的编码后便可进行发送。
类似的,QByteArray有一个data()方法来返回uchar指针。writeDatagram用于发送数据报,QHostAddress::Broadcast指使用广播模式。
void Sender::processPendingDatagram()
{
QByteArray datagram;
// 让datagram的大小为等待处理的数据报的大小,这样才能接收到完整的数据
datagram.resize(receiver->pendingDatagramSize());
// 接收数据报,将其存放到datagram中
receiver->readDatagram(datagram.data(), datagram.size());
ui->label_2->setText(datagram);
}
- 自定义的槽用于接收数据报。pendingDatagramSize()获得数据报的大小。readDatagram将数据存入一个QByteArray对象中。
2.上位机端(视频处理端)
Receiver::Receiver(QWidget *parent) :
QDialog(parent),
ui(new Ui::Receiver)
{
ui->setupUi(this);
sender = new QUdpSocket(this);
receiver = new QUdpSocket(this);
receiver->bind(45454, QUdpSocket::ShareAddress);
connect(receiver, &QUdpSocket::readyRead, this, &Receiver::processPendingDatagram);
}
与发送端类似。
void Receiver::processPendingDatagram()
{
QByteArray datagram;
// 让datagram的大小为等待处理的数据报的大小,这样才能接收到完整的数据
datagram.resize(receiver->pendingDatagramSize());
// 接收数据报,将其存放到datagram中
receiver->readDatagram(datagram.data(), datagram.size());
QByteArray decryptedByte;
decryptedByte=QByteArray::fromBase64(datagram.data());
QByteArray uncompressByte=qUncompress(decryptedByte);
QImage image;
image.loadFromData(uncompressByte);
Mat matImage(image.height(),image.width(),CV_8UC4,(void*)image.constBits(),
image.bytesPerLine());
}
- 解码解压后,需要将QImage对象转为Mat用于识别。这里使用的是Mat一个比较复杂的重载构造。第三个参数是数据类型,Qimage中的数据是ARGB888,占32位。第四个参数需要一个指向数据块的指针,constBits方法返回一个void指针。第四个参数需要一行的数据量,bytePerLine()方法可以获取。
Mat matImage_gray;
char itc[10];
if(frameCount >= 5)
{
cvtColor(matImage, matImage_gray, CV_BGR2GRAY);//转为灰度图
equalizeHist(matImage_gray, matImage_gray);//直方图均衡化,增加对比度方便处理
CascadeClassifier face_cascade; //载入分类器
if (!face_cascade.load(FACE_CLASSIFIER_PATH))
{
cout << "Load haarcascade_frontalface_alt failed!" << endl;
}
//检测关于脸部位置
face_cascade.detectMultiScale(matImage_gray, faceRect, 1.1, 2, 0, Size(30, 30));
for (size_t i = 0; i < faceRect.size(); i++)
{
//用矩形画出检测到的位置
rectangle(matImage, faceRect[i], Scalar(0, 0, 255));
sprintf(itc,"%d,%d",(faceRect[i].br().x+faceRect[i].tl().x)/2,
(faceRect[i].br().y+faceRect[i].tl().y)/2);
cout << itc <<endl;
sender->writeDatagram(itc,7,QHostAddress::Broadcast, 45455);
}
frameCount=0;
cvtColor(matImage,matImage,CV_BGR2RGB); //BGRtoRGB
image = QImage((unsigned char *)(matImage.data),
matImage.cols,matImage.rows,
QImage::Format_RGB888);
//sender->write()
}
else
{
for (size_t i = 0; i < faceRect.size(); i++)
{
//用矩形画出检测到的位置
rectangle(matImage, faceRect[i], Scalar(0, 0, 255));
}
cvtColor(matImage,matImage,CV_BGR2RGB); //BGRtoRGB
image = QImage((unsigned char *)(matImage.data),
matImage.cols,matImage.rows,
QImage::Format_RGB888);
frameCount++;
}
ui->label->setPixmap(QPixmap::fromImage(image));
ui->label->resize(image.width(),image.height());
}
这里使用机器学习算法级联增强分类器和Haar特征图像模型。opencv官方例程中已经帮我们训练好了,直接使用即可。
- 识别需要一定时间,这里每5帧处理一次。
- 宏FACE_CLASSIFIER_PATH是官方例程中的xml文件路径
- 将检测到的(人脸)矩形中心传回下位机端
3.实验结果
ok,实验结果还是很成功的,下一步就是移植到树莓派上了。