打算在樹莓派上挂載攝像頭,通過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,實驗結果還是很成功的,下一步就是移植到樹莓派上了。