1. 程式人生 > 實用技巧 >Qt音視訊開發38-USB攝像頭解碼linux方案

Qt音視訊開發38-USB攝像頭解碼linux方案

一、前言

做嵌入式linux上的開發很多年了,扳手指頭算算,也起碼9年了,陸陸續續做過很過諸如需要讀取外接的USB攝像頭或者CMOS攝像機的程式,實時採集視訊,將影象傳到前端,或者對影象進行人臉分析處理,最開始嘗試的就是QCamera來處理,直接歇菜放棄,後面通過搜尋發現都說要用v4l2視訊框架來進行,於是東搞搞西搞搞嘗試了很多次,終於整出來了,前後完善了好幾年,無論寫什麼程式,發現要簡簡單單的實現基礎的功能,都是非常快速而且容易的,但是想要做得好做得精,要花不少的精力時間去完善,適應各種不同的場景,比如就說用v4l2載入攝像頭這個,需要指定裝置檔案來讀取,而現場不可能讓使用者來給你指定,頻繁的拔插也會導致裝置檔名的改動,所以必須找到一個機制自動尋找你想要的攝像機的裝置檔名稱,比如開個定時器去呼叫linux命令來處理,甚至在不同的系統平臺上要執行的命令還有些許的區別,如果本地有多個攝像頭還需要區分左右之類的時候,那就只能通過斷電先後上電順序次序來區分了。

linux方案處理流程:

  1. 呼叫封裝的函式findCamera實時查詢攝像頭裝置檔名。
  2. 呼叫::open函式開啟裝置檔案。
  3. 呼叫封裝的函式initCamera初始化攝像頭引數(圖片格式、解析度等)。
  4. 呼叫::select函式從緩衝區取出一個緩衝幀。
  5. 緩衝幀資料是yuyv格式的,需要轉換rgb24再轉成QImage。
  6. 拿到圖片進行繪製、人臉分析等。
  7. 關閉裝置檔案。

二、功能特點

  1. 同時支援windows、linux、嵌入式linux上的USB攝像頭實時採集。
  2. 支援多路USB攝像頭多執行緒實時採集。
  3. 在嵌入式linux裝置上,自動查詢USB裝置檔案並載入。
  4. 可手動設定裝置檔名稱,手動設定後按照手動設定的裝置檔案載入。
  5. 在嵌入式linux裝置上支援人臉識別介面,實時繪製人臉框。
  6. 具有開啟、暫停、繼續、關閉、截圖等常規功能。
  7. 可設定兩路OSD標籤,分別設定文字、顏色、字號、位置等。
  8. 可作為視訊監控系統使用。

三、效果圖

四、相關站點

  1. 國內站點:https://gitee.com/feiyangqingyun/QWidgetDemo
  2. 國際站點:https://github.com/feiyangqingyun/QWidgetDemo
  3. 個人主頁:https://blog.csdn.net/feiyangqingyun
  4. 知乎主頁:https://www.zhihu.com/people/feiyangqingyun/
  5. 體驗地址: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;
}