Qt音視訊開發38-USB攝像頭解碼linux方案
阿新 • • 發佈:2020-10-21
一、前言
做嵌入式linux上的開發很多年了,扳手指頭算算,也起碼9年了,陸陸續續做過很過諸如需要讀取外接的USB攝像頭或者CMOS攝像機的程式,實時採集視訊,將影象傳到前端,或者對影象進行人臉分析處理,最開始嘗試的就是QCamera來處理,直接歇菜放棄,後面通過搜尋發現都說要用v4l2視訊框架來進行,於是東搞搞西搞搞嘗試了很多次,終於整出來了,前後完善了好幾年,無論寫什麼程式,發現要簡簡單單的實現基礎的功能,都是非常快速而且容易的,但是想要做得好做得精,要花不少的精力時間去完善,適應各種不同的場景,比如就說用v4l2載入攝像頭這個,需要指定裝置檔案來讀取,而現場不可能讓使用者來給你指定,頻繁的拔插也會導致裝置檔名的改動,所以必須找到一個機制自動尋找你想要的攝像機的裝置檔名稱,比如開個定時器去呼叫linux命令來處理,甚至在不同的系統平臺上要執行的命令還有些許的區別,如果本地有多個攝像頭還需要區分左右之類的時候,那就只能通過斷電先後上電順序次序來區分了。
linux方案處理流程:
- 呼叫封裝的函式findCamera實時查詢攝像頭裝置檔名。
- 呼叫::open函式開啟裝置檔案。
- 呼叫封裝的函式initCamera初始化攝像頭引數(圖片格式、解析度等)。
- 呼叫::select函式從緩衝區取出一個緩衝幀。
- 緩衝幀資料是yuyv格式的,需要轉換rgb24再轉成QImage。
- 拿到圖片進行繪製、人臉分析等。
- 關閉裝置檔案。
二、功能特點
- 同時支援windows、linux、嵌入式linux上的USB攝像頭實時採集。
- 支援多路USB攝像頭多執行緒實時採集。
- 在嵌入式linux裝置上,自動查詢USB裝置檔案並載入。
- 可手動設定裝置檔名稱,手動設定後按照手動設定的裝置檔案載入。
- 在嵌入式linux裝置上支援人臉識別介面,實時繪製人臉框。
- 具有開啟、暫停、繼續、關閉、截圖等常規功能。
- 可設定兩路OSD標籤,分別設定文字、顏色、字號、位置等。
- 可作為視訊監控系統使用。
三、效果圖
四、相關站點
- 國內站點:https://gitee.com/feiyangqingyun/QWidgetDemo
- 國際站點:https://github.com/feiyangqingyun/QWidgetDemo
- 個人主頁:https://blog.csdn.net/feiyangqingyun
- 知乎主頁:https://www.zhihu.com/people/feiyangqingyun/
- 體驗地址:https://blog.csdn.net/feiyangqingyun/article/details/97565652
五、核心程式碼
void CameraLinux::run()
{
while (!stopped) {
if (!cameraOk) {
msleep(10);
continue;
}
if (isPause) {
//這裡需要假設正常,暫停期間繼續更新時間
lastTime = QDateTime::currentDateTime();
msleep(10);
continue;
}
QImage image = readImage();
if (!image.isNull()) {
if (isSnap) {
emit snapImage(image);
isSnap = false;
}
if (findFaceOne) {
findFace(image);
}
if (findFaceRect) {
image = drawFace(image);
}
lastTime = QDateTime::currentDateTime();
emit receiveImage(image);
}
msleep(interval);
}
this->closeCamera();
this->initData();
}
QDateTime CameraLinux::getLastTime() const
{
return this->lastTime;
}
QString CameraLinux::getCameraName() const
{
return this->cameraName;
}
int CameraLinux::getCameraWidth() const
{
return this->cameraWidth;
}
int CameraLinux::getCameraHeight() const
{
return this->cameraHeight;
}
void CameraLinux::sleep(int msec)
{
if (msec > 0) {
QTime endTime = QTime::currentTime().addMSecs(msec);
while (QTime::currentTime() < endTime) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
}
}
void CameraLinux::initData()
{
stopped = false;
isPause = false;
isSnap = false;
cameraOk = false;
cameraHwnd = -1;
errorCount = 0;
}
void CameraLinux::readData()
{
QStringList cameraNames;
while (!process->atEnd()) {
//逐行讀取返回的結果 過濾video開頭的是攝像頭裝置檔案
QString line = process->readLine();
if (line.startsWith("video")) {
line = line.replace("\n", "");
cameraNames << QString("/dev/%1").arg(line);
}
}
if (cameraNames.count() > 0) {
cameraName = cameraNames.first();
emit receiveCamera(cameraNames);
qDebug() << TIMEMS << cameraNames;
}
}
bool CameraLinux::initCamera()
{
//如果沒有指定裝置檔名稱(預設auto)則查詢
if (cameraName == "auto") {
findCamera();
}
//延時判斷是否獲取到了裝置檔案
sleep(300);
return openCamera();
}
void CameraLinux::findCamera()
{
if (process->state() == QProcess::NotRunning) {
process->start("ls /dev/");
}
}
bool CameraLinux::openCamera()
{
#ifdef Q_OS_LINUX
if (cameraName.length() > 5) {
cameraHwnd = ::open(cameraName.toUtf8().data(), O_RDWR | O_NONBLOCK, 0);
}
if (cameraHwnd < 0) {
qDebug() << TIMEMS << "open camera error";
return false;
}
//查詢裝置屬性
struct v4l2_capability capability;
if (::ioctl(cameraHwnd, VIDIOC_QUERYCAP, &capability) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_QUERYCAP";
::close(cameraHwnd);
return false;
}
if (!(capability.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
qDebug() << TIMEMS << "it is not a video capture device";
::close(cameraHwnd);
return false;
}
if (!(capability.capabilities & V4L2_CAP_STREAMING)) {
qDebug() << TIMEMS << "it can not streaming";
::close(cameraHwnd);
return false;
}
if (capability.capabilities == 0x4000001) {
qDebug() << TIMEMS << "capabilities" << "V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING";
}
//設定視訊輸入源
int input = 0;
if (::ioctl(cameraHwnd, VIDIOC_S_INPUT, &input) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_S_INPUT";
::close(cameraHwnd);
return false;
}
//設定圖片格式和解析度
struct v4l2_format format;
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//多種格式 V4L2_PIX_FMT_YUV420 V4L2_PIX_FMT_YUYV(422) V4L2_PIX_FMT_RGB565
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
//部分硬體花屏要設定成 V4L2_FIELD_NONE
format.fmt.pix.field = V4L2_FIELD_INTERLACED;
format.fmt.pix.width = cameraWidth;
format.fmt.pix.height = cameraHeight;
int bpp = 16;
//format.fmt.pix.bytesperline = width * bpp / 8;
//format.fmt.pix.sizeimage = cameraWidth * cameraHeight * bpp / 8;
if (::ioctl(cameraHwnd, VIDIOC_S_FMT, &format) < 0) {
::close(cameraHwnd);
return false;
}
//檢視圖片格式和解析度,判斷是否設定成功
if (::ioctl(cameraHwnd, VIDIOC_G_FMT, &format) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_G_FMT";
::close(cameraHwnd);
return false;
}
//重新列印下寬高看下是否真正設定成功
struct v4l2_pix_format pix = format.fmt.pix;
quint32 pixelformat = pix.pixelformat;
qDebug() << TIMEMS << "cameraWidth" << cameraWidth << "cameraHeight" << cameraHeight << "width" << pix.width << "height" << pix.height;
qDebug() << TIMEMS << "pixelformat" << QString("%1%2%3%4").arg(QChar(pixelformat & 0xFF)).arg(QChar((pixelformat >> 8) & 0xFF)).arg(QChar((pixelformat >> 16) & 0xFF)).arg(QChar((pixelformat >> 24) & 0xFF));
//重新設定寬高為真實的寬高
cameraWidth = pix.width;
cameraHeight = pix.height;
//設定幀格式
struct v4l2_streamparm streamparm;
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 25;
streamparm.parm.capture.capturemode = 0;
if (::ioctl(cameraHwnd, VIDIOC_S_PARM, &streamparm) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_S_PARM";
::close(cameraHwnd);
return false;
}
if (::ioctl(cameraHwnd, VIDIOC_G_PARM, &streamparm) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_G_PARM";
::close(cameraHwnd);
return false;
}
//申請和管理緩衝區
struct v4l2_requestbuffers requestbuffers;
requestbuffers.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
requestbuffers.memory = V4L2_MEMORY_MMAP;
requestbuffers.count = 1;
if (::ioctl(cameraHwnd, VIDIOC_REQBUFS, &requestbuffers) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_REQBUFS";
::close(cameraHwnd);
return false;
}
buff_yuv422 = (uchar *)malloc(cameraWidth * cameraHeight * bpp / 8);
buff_yuv420 = (uchar *)malloc(cameraWidth * cameraHeight * bpp / 8);
buff_rgb24 = (uchar *)malloc(cameraWidth * cameraHeight * 24 / 8);
buff_img = (ImgBuffer *)calloc(1, sizeof(ImgBuffer));
if (buff_img == NULL) {
qDebug() << TIMEMS << "error in calloc";
::close(cameraHwnd);
return false;
}
struct v4l2_buffer buffer;
for (int index = 0; index < 1; index++) {
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
buffer.index = index;
if (::ioctl(cameraHwnd, VIDIOC_QUERYBUF, &buffer) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_QUERYBUF";
::free(buff_img);
::close(cameraHwnd);
return false;
}
buff_img[index].length = buffer.length;
buff_img[index].start = (quint8 *)mmap(NULL, buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, cameraHwnd, buffer.m.offset);
if (MAP_FAILED == buff_img[index].start) {
qDebug() << TIMEMS << "error in mmap";
::free(buff_img);
::close(cameraHwnd);
return false;
}
//把緩衝幀放入佇列
if (::ioctl(cameraHwnd, VIDIOC_QBUF, &buffer) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_QBUF";
for (int i = 0; i <= index; i++) {
munmap(buff_img[i].start, buff_img[i].length);
}
::free(buff_img);
::close(cameraHwnd);
return false;
}
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (::ioctl(cameraHwnd, VIDIOC_STREAMON, &type) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_STREAMON";
for (int i = 0; i < 1; i++) {
munmap(buff_img[i].start, buff_img[i].length);
}
::free(buff_img);
::close(cameraHwnd);
return false;
}
cameraOk = true;
#endif
qDebug() << TIMEMS << "open camera ok";
return cameraOk;
}
void CameraLinux::closeCamera()
{
#ifdef Q_OS_LINUX
if (cameraOk && buff_img != NULL) {
//停止攝像頭採集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (::ioctl(cameraHwnd, VIDIOC_STREAMOFF, &type) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_STREAMOFF";
}
//解除記憶體對映
for (int i = 0; i < 1; i++) {
munmap((buff_img)[i].start, (buff_img)[i].length);
}
//關閉裝置檔案
::close(cameraHwnd);
qDebug() << TIMEMS << "close camera ok";
}
//釋放資源
::free(buff_img);
buff_img = NULL;
::free(buff_yuv422);
buff_yuv422 = NULL;
::free(buff_yuv420);
buff_yuv420 = NULL;
::free(buff_rgb24);
buff_rgb24 = NULL;
cameraOk = false;
cameraHwnd = -1;
#endif
}
int CameraLinux::readFrame()
{
int index = -1;
#ifdef Q_OS_LINUX
//等待攝像頭採集到一楨資料
for (;;) {
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(cameraHwnd, &fds);
tv.tv_sec = 2;
tv.tv_usec = 0;
int r = ::select(cameraHwnd + 1, &fds, NULL, NULL, &tv);
if (-1 == r) {
if (EINTR == errno) {
continue;
}
return -1;
} else if (0 == r) {
return -1;
} else {
//採集到一張圖片 跳出迴圈
break;
}
}
//從緩衝區取出一個緩衝幀
struct v4l2_buffer buffer;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
if (::ioctl(cameraHwnd, VIDIOC_DQBUF, &buffer) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_DQBUF";
return -1;
}
memcpy(buff_yuv422, (uchar *)buff_img[buffer.index].start, buff_img[buffer.index].length);
//將取出的緩衝幀放回緩衝區
if (::ioctl(cameraHwnd, VIDIOC_QBUF, &buffer) < 0) {
qDebug() << TIMEMS << "error in VIDIOC_QBUF";
return -1;
}
index = buffer.index;
#endif
return index;
}