本文经过拜读师兄一路调bug的实验过程,深知入门不易,在这接着总结一些嵌入式配置opencv等的大致思路。同时对摄像头读取操作和QT中opencv处理作详细的记录。
附上其文章,建议常读:i.MX6开发板 嵌入式linux开发
上面笔记记录一些嵌入式内核配置opencv的过程,同时简述opencv+qt在嵌入式内核中运行的配置;
本次实验的ARM板子是IMX6开发板,烧写uboot及linux内核加qt4.7的系统,具体的烧写过程这里不阐述。配置见相关文档,当然也可以自己编译。
用usb烧写完上述系统之后配置相关显示模式即可。当然为了便于显示自己编写的qt程序,可以修改相关的启动文件,使得默认开机启动的程序是特定的程序或者不自启动。
vi /etc/init.d/rcS
配置完成之后可以随时用超级终端复制相应的qt程序到开发板中运行。
需要在Ubuntu上配置好和开发板相同的编译器arm-linux-gcc-4.3.2,同时在qtcreator上配置相应的编译器arm-linux-gcc-4.3.2(直接在终端make也行),这样生成的可执行文件才能够复制到ARM开发板中运行。需要注意的是,在开发板中运行的qte系统是qt4.7,所以在qtcreator中编译时也需要选择qt4.7。
在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程序。
调用opencv会遇到两个问题
1. Linux中无法直接调用摄像头
2. 无法使用opencv的img.show()函数
解决思路:
1. opencv无法直接调用摄像头时,需要调用linux内核中摄像头V4L2驱动,进行一定图像格式转换之后才能够被opencv调用,具体的调用和转换细节参考师兄的博客(太大佬了),在这个过程中需要读取和配置摄像头的一些参数。
2. show()函数无法调用和其实现方式有关,简单来讲是ARM中的linux内核无法提供相关的KDE桌面环境,这里有两种解决思路,配置KDE相关的环境或者将图像转换成qt中img显示相关的格式并且用其控件显示。既然已经用了qt,显然在qt中作迂回会比较低风险。
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()函数,该函数主要是对底层的文件进行配置。完成的功能有:打开设备-> 检查和设置设备属性-> 设置帧格式-> 设置一种输入输出方法(缓冲区管理)
之后需要进行)-> 循环获取数据-> 关闭设备控制。
请求驱动申请内存去缓冲图像
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 确定可用数据的起始地址和范围。
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得到的地址赋给指针集,只是将驱动返回的物理地址与应用层的虚拟映射地址重映射。
将各个缓冲区添加到ready_q中,等待将图像输出到这些缓冲区
//6 queue for(n_buffers = 0;n_buffers
ioctl(fd,VIDIOC_QBUF,&buf)表明将buf缓冲区(指针)添加到ready_q中,使得驱动有权限向里面缓冲图像。
type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if(ioctl(fd,VIDIOC_STREAMON,&type) == -1) { // printf("stream on error\n"); return FALSE; }
读取图像操作为设置一个时间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);
将不同空间格式图像作转换,使得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
此部分包含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用一个图像大小的控件将其显示出来。
OPENCV中的show()函数无法调用和其实现方式有关,简单来讲是ARM中的linux内核无法提供相关的KDE桌面环境,这里有两种解决思路,配置KDE相关的环境或者将图像转换成qt中img显示相关的格式并且用其控件显示。既然已经用了qt,显然在qt中作迂回会比较低风险。
经过将图像格式转换为OPENCV能够处理的格式之后,只需要将指针传到OPENCV即可进行图像处理部分,图像处理完之后,由于OPENCV中的imshow函数无法在没有KDE支持的Linux系统中显示,故需要将OPENCV处理完之后的结果转换为Qimage用一个图像大小的控件将其显示出来。
Qt在虚拟Ubuntu中进行,编写Qt程序的编译器即使用的是开发板的相应交叉编译器。该编译器需要提前配置好。
编译后生成的交叉编译程序无法在当前的虚拟机运行,需要传送到开发板中的文件系统中运行即可。