ARM与Qt及openCV的思路总结

本文经过拜读师兄一路调bug的实验过程,深知入门不易,在这接着总结一些嵌入式配置opencv等的大致思路。同时对摄像头读取操作和QT中opencv处理作详细的记录。

附上其文章,建议常读:i.MX6开发板 嵌入式linux开发

上面笔记记录一些嵌入式内核配置opencv的过程,同时简述opencv+qt在嵌入式内核中运行的配置;

一、思路

1. ARM+Qte的配置

  本次实验的ARM板子是IMX6开发板,烧写uboot及linux内核加qt4.7的系统,具体的烧写过程这里不阐述。配置见相关文档,当然也可以自己编译。

  用usb烧写完上述系统之后配置相关显示模式即可。当然为了便于显示自己编写的qt程序,可以修改相关的启动文件,使得默认开机启动的程序是特定的程序或者不自启动。

vi /etc/init.d/rcS

配置完成之后可以随时用超级终端复制相应的qt程序到开发板中运行。

 

2. Ubuntu虚拟机上qt程序的编写编译

需要在Ubuntu上配置好和开发板相同的编译器arm-linux-gcc-4.3.2,同时在qtcreator上配置相应的编译器arm-linux-gcc-4.3.2(直接在终端make也行),这样生成的可执行文件才能够复制到ARM开发板中运行。需要注意的是,在开发板中运行的qte系统是qt4.7,所以在qtcreator中编译时也需要选择qt4.7。

 

3. opencv的源码编译以及环境配置

在Ubuntu虚拟机中编译opencv2源码并复制到ARM中:

想要在ARM开发板上运行opencv开源库相关的程序,需要先在Ubuntu虚拟机上用与ARM相同的编译器arm-linux-gcc-4.3.2编译opencv源码,得到的.so文件复制到开发板上的/lib目录。具体的编译过程和对make需要设置的东西在其他博客中有提及,需要对makefile文件做一定的修改。opencv的版本不能过高,否则编译器无法完成编译。

关于ARM开发板那种的opencv编译完成之后的.so文件存放的目录,这一点需要根据对opencv编译时的设置有关,如果配置生成了相应的pkgconfig环境变量,则可以根据所填写的目录将lib中的.so文件复制到相应的目录,如果嫌麻烦,直接复制到./lib即可运行。

由于前面师兄的工作,只需要:

1.将opencv库编译后的文件(包含include、lib/*.so文件等)复制到Ubuntu虚拟机的usr/local...相关目录

2.将opencv库编译后的lib文件中的.so复制到arm开发板的lib目录

3.在Ubuntu虚拟机上编写qt程序时将相关的库路径以及lib路径写入.pro文件中

即可完成在Ubuntu虚拟机的Qtcreator中编写opencv相关的qt程序,然后用arm-linux-gcc-4.3.2+qt4.7编译得到的可执行文件(无法在Ubuntu虚拟机中运行),然后将debug中的可执行文件复制到arm开发板中运行,即可完成在arm开发板中运行opencv相关的qt程序。

 

4. ARM中实现opencv调用问题

调用opencv会遇到两个问题

1. Linux中无法直接调用摄像头

2. 无法使用opencv的img.show()函数

解决思路:

1. opencv无法直接调用摄像头时,需要调用linux内核中摄像头V4L2驱动,进行一定图像格式转换之后才能够被opencv调用,具体的调用和转换细节参考师兄的博客(太大佬了),在这个过程中需要读取和配置摄像头的一些参数。

2. show()函数无法调用和其实现方式有关,简单来讲是ARM中的linux内核无法提供相关的KDE桌面环境,这里有两种解决思路,配置KDE相关的环境或者将图像转换成qt中img显示相关的格式并且用其控件显示。既然已经用了qt,显然在qt中作迂回会比较低风险。

 

二、ARM中v4l2配置

 

1. v4l2处理过程--获取设备信息

v4l2的命令码:v4l2 编程接口(一) — ioctl

定义的一些格式结构的解释

struct v4l2_buffer  enqueue  , dequeue ;  //定义出入队的操作结构体成员

v4l2_capability cap;  /* 查询打开的设备是否属于摄像头:设备video不一定是摄像头*/

format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE ;//定义v4l2的处理格式
format.fmt.pix.width  = Width;
format.fmt.pix.height = Hight;
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV ;  //摄像头支持的格式

 

打开并获取设备信息
 

fd= open(videodevname , O_RDWR);//打开摄像头,将文件描述符传给fd

ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);//获取设备信息

 

VIDIOC_QUERYCAP定义

VIDIOC_QUERYCAP 命令通过结构 v4l2_capability 获取设备支持的操作模式:
struct v4l2_capability {
    __u8    driver[16];     /* i.e. "bttv" */
    __u8    card[32];       /* i.e. "Hauppauge WinTV" */
    __u8    bus_info[32];   /* "PCI:" + pci_name(pci_dev) */
    __u32   version;        /* should use KERNEL_VERSION() */
    __u32    capabilities;   /* Device capabilities */
    __u32    reserved[4];
};

printf("Driver    Name: %s\n", cap.driver);//print设备的驱动名字

 

查询摄像头可捕捉的图片类型
/* 查询摄像头可捕捉的图片类型,VIDIOC_ENUM_FMT: 枚举摄像头帧格式 */
 

    struct v4l2_fmtdesc fomat;
    fomat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 指定需要枚举的类型
    for (int i = 0; ; i++)                      // 有可能摄像头支持的图片格式不止一种
    {
        fomat.index = i;
        ret = ioctl(fd, VIDIOC_ENUM_FMT, &fomat);
        if (-1 == ret)                      // 获取所有格式完成
        {
            break;
        }

/* 打印摄像头图片格式 */
        printf("Picture Format: %s\n", fomat.description);

 

查询对应图像格式所支持的分辨率 

        struct v4l2_frmsizeenum frmsize;
        frmsize.pixel_format = fomat.pixelformat;
        for (int j = 0; ; j++)                  // 该格式支持分辨率不止一种
        {
            frmsize.index = j;
            ret = ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize);
            if (-1 == ret)                  // 获取所有图片分辨率完成
            {
                break;
            }

            /* 打印图片分辨率 */
            printf("width: %d height: %d\n",
                    frmsize.discrete.width,frmsize.discrete.height);
        }

 

查询并显示所有(video)支持的格式

struct v4l2_fmtdesc

{
u32 index; // 要查询的格式序号,应用程序设置
enum v4l2_buf_type type; // 帧类型,应用程序设置
u32 flags; // 是否为压缩格式
u8 description[32]; // 格式名称
u32 pixelformat; // 格式
u32 reserved[4]; // 保留
};

好像和获取分辨率前面的操作重复了

fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    printf("Support format: \n");
    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) != -1){
        printf("\t%d. %s\n",fmtdesc.index+1,fmtdesc.description);//print格式
        fmtdesc.index++;
    }

 

 配置视频格式

    v4l2_format fmt,fmtack;    
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = img_width;//320
    fmt.fmt.pix.height = img_height;//240
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //*********V4L2_PIX_FMT_MJPEG**
    fmt.fmt.pix.field = V4L2_FIELD_NONE;

    ioctl(fd, VIDIOC_S_FMT, &fmt)

v4l2_format 结构体用来设置摄像头的视频制式、帧格式等,在设置这个参数时应先填 好 v4l2_format 的各个域,如 type(传输流类型),fmt.pix.width(宽),

fmt.pix.heigth(高),fmt.pix.field(采样区域,如隔行采样),fmt.pix.pixelformat(采

样类型,如 YUV4:2:2),然后通过 VIDIO_S_FMT 操作命令设置视频捕捉格式。

 设置预期的帧率

    enum   v4l2_buf_type type;              //帧类型    
    setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;                  //设置预期的帧率,实际值不一定能达到
    setfps.parm.capture.timeperframe.denominator = 30;          //fps=30/1=30
    setfps.parm.capture.timeperframe.numerator = 1;

V4L2 的相关定义包含在头文件 中。

以上操作完成了对摄像头设备的初始化配置。该配置主要用到的了ioctl()函数,该函数主要是对底层的文件进行配置。完成的功能有:打开设备-> 检查和设置设备属性-> 设置帧格式-> 设置一种输入输出方法(缓冲区管理)

之后需要进行)-> 循环获取数据-> 关闭设备控制。

 

2.设置v4l2缓冲buffer

请求驱动申请内存去缓冲图像

struct v4l2_requestbuffers 与 VIDIOC_REQBUFS
VIDIOC_REQBUFS 命令通过结构 v4l2_requestbuffers 请求驱动申请一片连续的内存用于缓存视频信息:

struct v4l2_requestbuffers {
    __u32                   count;
    enum v4l2_buf_type      type;
    enum v4l2_memory        memory;
    __u32                   reserved[2];
};
其中
enum v4l2_memory {
    V4L2_MEMORY_MMAP             = 1,
    V4L2_MEMORY_USERPTR          = 2,
    V4L2_MEMORY_OVERLAY          = 3,
};

count 指定根据图像占用空间大小申请的缓存区个数,type 为视频捕获模式,memory 为内存区的使用方式。

struct v4l2_requestbuffers req;         //向驱动申请帧缓冲的请求,里面包含申请的个数
req.count = 30;  //20190818 4->12!!!!!这里大概是缓冲区的个数,从后面的过程看,多了好像也没用
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1)        //开启内存映射或用户指针I/O

 struct v4l2_buffer与 VIDIOC_QUERYBUF
VIDIOC_QUERYBUF 命令通过结构 v4l2_buffer 查询驱动申请的内存区信息

struct v4l2_buffer buf;                 //代表驱动中的一帧 


struct v4l2_buffer {
	__u32                   index;
	enum v4l2_buf_type      type;
	__u32                   bytesused;
	__u32                   flags;
	enum v4l2_field         field;
	struct timeval          timestamp;
	struct v4l2_timecode    timecode;
	__u32                   sequence;
 
	/* memory location */
	enum v4l2_memory        memory;
	union {
	        __u32           offset;
	        unsigned long   userptr;
	} m;
	__u32                   length;
	__u32                   input;
	__u32                   reserved;
};
struct v4l2_buffer
{
u32 index; //buffer 序号
enum v4l2_buf_type type; //buffer 类型
u32 byteused; //buffer 中已使用的字节数
u32 flags; // 区分是MMAP 还是USERPTR
enum v4l2_field field;
struct timeval timestamp; // 获取第一个字节时的系统时间
struct v4l2_timecode timecode;
u32 sequence; // 队列中的序号
enum v4l2_memory memory; //IO 方式,被应用程序设置
union m
{
u32 offset; // 缓冲帧地址,只对MMAP 有效
unsigned long userptr;
};
u32 length; // 缓冲帧长度
u32 input;
u32 reserved;
};

index 为缓存编号,type 为视频捕获模式,bytesused 为缓存已使用空间大小,flags 为缓存当前状态(常见值有 V4L2_BUF_FLAG_MAPPED | V4L2_BUF_FLAG_QUEUED | V4L2_BUF_FLAG_DONE,分别代表当前缓存已经映射、缓存可以采集数据、缓存可以提取数据),timestamp 为时间戳,sequence为缓存序号,memory 为缓存使用方式,offset 为当前缓存与内存区起始地址的偏移,length 为缓存大小,reserved 一般用于传递物理地址值。
另外 VIDIOC_QBUF 和 VIDIOC_DQBUF 命令都采用结构 v4l2_buffer 与驱动通信:VIDIOC_QBUF 命令向驱动传递应用程序已经处理完的缓存,即将缓存加入空闲可捕获视频的队列,传递的主要参数为 index;VIDIOC_DQBUF 命令向驱动获取已经存放有视频数据的缓存,v4l2_buffer 的各个域几乎都会被更新,但主要的参数也是 index,应用程序会根据 index 确定可用数据的起始地址和范围。

 

3.为缓冲区重映射mmap


buffer *buffers;     //buffers 指针记录缓冲帧
buffers = (buffer *)malloc(req.count * sizeof(*buffers));
 typedef struct _buffer			//定义缓冲区结构体
{
    void *start;
    unsigned int length;
}buffer;

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    unsigned int n_buffers;
    for(n_buffers = 0;n_buffers < req.count; n_buffers++)
    {
        //struct v4l2_buffer buf = {0};

        buf.index = n_buffers;
        if(ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1){ 
// 查询已经分配的V4L2的视频缓冲区的相关信息,包括视频缓冲区的使用状态、在内核空间的偏移地址、缓冲区长度等。
            printf("Querying Buffer error\n");
            return FALSE;
        }
        buffers[n_buffers].length = buf.length;
        buffers[n_buffers].start = mmap (NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

        if(buffers[n_buffers].start == MAP_FAILED){
            printf("buffer map error\n");
            return FALSE;
            }
        //printf("Length: %d\nAddress: %p\n", buf.length, buffers);
        //printf("Image Length: %d\n", buf.bytesused);
    }

将得到的buffer缓冲区们mmap得到的地址赋给指针集,只是将驱动返回的物理地址与应用层的虚拟映射地址重映射。

4.将缓冲区buffer添加到待缓冲队列中 

将各个缓冲区添加到ready_q中,等待将图像输出到这些缓冲区

    //6 queue
    for(n_buffers = 0;n_buffers 

ioctl(fd,VIDIOC_QBUF,&buf)表明将buf缓冲区(指针)添加到ready_q中,使得驱动有权限向里面缓冲图像。

5.打开视频流,开启相机

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if(ioctl(fd,VIDIOC_STREAMON,&type) == -1)
    { //
        printf("stream on error\n");
        return FALSE;
    }

6. 读取缓冲区的图像并处理队列

 读取图像操作为设置一个时间slot函数,设置slot函数按照一定的帧率运行,读取缓冲区的数据并作处理,整理队列。

    enqueue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE ;
    dequeue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE ;
    enqueue.memory = V4L2_MEMORY_MMAP ;
    dequeue.memory = V4L2_MEMORY_MMAP ;
void MainWindow::next_frame()
{
    static int ccccc=1;
    ccccc++;
    printf(" frame is %d\n",ccccc);

    t = (double)cvGetTickCount();
    CvMat cvmat;
    int ff =ioctl(fd,VIDIOC_DQBUF,&dequeue);
    if(ff==-1)
    {
        printf("frame error ~~~~~~\n");
    }
    else
    {
        yuyv_2_y();
        enqueue.index = dequeue.index ;
        int fg=ioctl(fd,VIDIOC_QBUF,&enqueue);
        if(fg==-1)
            printf("frame error ~~~~~~\n");
        else
        {
            printf("new frame\n");
            cvmat = cvMat(img_height,img_width,CV_8UC1,(void*)frame_buffer);//CV_8UC3
            Mat b = Mat(&cvmat, true); //CvMat转Mat
            if(processFlag)
                b=imgProcess(b);
            QImage img1 = QImage((const unsigned char*)(b.data),b.cols,b.rows,QImage::Format_Indexed8);
            ui->label->setPixmap(QPixmap::fromImage(img1));
        }
    }

    t=(double)cvGetTickCount()-t;
    printf("used time is %gms\n",(t/(cvGetTickFrequency()*1000)));
}

VIDIOC_QBUF 和 VIDIOC_DQBUF 命令都采用结构 v4l2_buffer 与驱动通信:VIDIOC_QBUF 命令向驱动传递应用程序已经处理完的缓存,即将缓存加入空闲可捕获视频的队列,传递的主要参数为 index;VIDIOC_DQBUF 命令向驱动获取已经存放有视频数据的缓存,v4l2_buffer 的各个域几乎都会被更新,但主要的参数也是 index,应用程序会根据 index 确定可用数据的起始地址和范围。

简单来说:

VIDIOC_QBUF// 把特定缓冲区放入队列(在处理过程中,该缓冲区中的图像已经读取)

VIDIOC_DQBUF// 从队列中取出某一个已经更新完图像缓冲区

在上一段程序中,应该在yuyv_2_y()函数前,将dequeue.index地址传给buffers[dequeue.index],使得buffers寻址的地址是返回的dequeue的地址。

 int ff =ioctl(fd,VIDIOC_DQBUF,&dequeue);//返回包含图像指针的dequeue.index
        yuyv_2_y();//图像(格式转换)处理函数,在这调用buffers[]读取图象时,应该寻址为buffers[dequeue.index].start,为图像首重映射地址
        enqueue.index = dequeue.index ;
        int fg=ioctl(fd,VIDIOC_QBUF,&enqueue);

7. 图像格式转换

将不同空间格式图像作转换,使得opencv能够处理。

void yuyv_2_y()
{
    int           i,j;
    unsigned char y1,y2;
    char    *pointer;
    pointer = (char *)buffers[0].start;//这里不应该是0,而应该是dequeue.index
    for(i=0;i

 

8. 关闭摄像头,结束图像队列

此部分包含mnumap()函数,以及VIDIOC_STREAMOFF调用驱动关闭视频流。而其中的队列会随着摄像头的释放而处理。

void v4l2_close()
{
    int i=0;
    for(i=0; i

以上代码的完整板在其他博客中有记录,这里仅作学习整理,推荐两个优秀的大牛:

驱动程序的详细解读

编程接口ioctl

Linux的V4L2编程

以上为v4l2驱动读取图像的全过程,经过将图像格式转换为opencv能够处理的格式之后,只需要将指针传到opencv即可进行图像处理部分,图像处理完之后,由于opencv中的imshow函数无法在没有KDE支持的Linux系统中显示,故需要将opencv处理完之后的结果转换为Qimage用一个图像大小的控件将其显示出来。

 

三、Qt程序及图像处理

用QImage控件将图像显示

    OPENCV中的show()函数无法调用和其实现方式有关,简单来讲是ARM中的linux内核无法提供相关的KDE桌面环境,这里有两种解决思路,配置KDE相关的环境或者将图像转换成qt中img显示相关的格式并且用其控件显示。既然已经用了qt,显然在qt中作迂回会比较低风险。

经过将图像格式转换为OPENCV能够处理的格式之后,只需要将指针传到OPENCV即可进行图像处理部分,图像处理完之后,由于OPENCV中的imshow函数无法在没有KDE支持的Linux系统中显示,故需要将OPENCV处理完之后的结果转换为Qimage用一个图像大小的控件将其显示出来。

Qt程序编写

    Qt在虚拟Ubuntu中进行,编写Qt程序的编译器即使用的是开发板的相应交叉编译器。该编译器需要提前配置好。

编译后生成的交叉编译程序无法在当前的虚拟机运行,需要传送到开发板中的文件系统中运行即可。

 

 

 

 

你可能感兴趣的:(嵌入式,ARM,openCV,Qt)