这段时间一直在做Linux视觉相关的学习,一直弄不清楚V4L2相关的操作。所以本着学习的态度,制作一个Linux小相机。本次工程是基于正原子的i.IMX6null开发板+USB免驱摄像头+任意显示屏。至于为什么要转为Mat格式呢?因为在后期进行深度学习时,利用OpenCV来处理是非常方便的。
目录
1、打开摄像头,利用open函数来打开,fd句柄用于判断开启状态。
2、获取摄像头支持的格式,可以查看摄像头支持的格式,用于第三步设置摄像头抓图格式
3、设置摄像头相关格式,相关格式可以在第二部打印出来,我这里设置的是MJPEG格式,大家根据自己摄像头来设置。
4、申请内核缓冲区队列映射到用户空间,也就是向内核申请一个位置用于存放数据
6、开始采集,VIDIOC_STREAMON(开始采集写数据到队列中)
一、编写V4L2相关代码
编写这一部分的代码需要一定的文件IO基础,能够明白open、ioctl等等基础知识。整体流程就是
1、打开摄像头,利用open函数来打开,fd句柄用于判断开启状态。
if(argc != 2)
{
printf("plese input camrea_dev!\n");
return -1;
}
printf("按下key0进行拍照!\n");
//1、打开摄像头
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("open camrea fail!");
return -1;
}
2、获取摄像头支持的格式,可以查看摄像头支持的格式,用于第三步设置摄像头抓图格式
struct v4l2_fmtdesc vfmts;
vfmts.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
int ret = ioctl(fd, VIDIOC_ENUM_FMT, &vfmts);
if(ret < 0)
{
perror("获取设备支持格式VIDIOC_ENUM_FMT失败");
return -1;
}
printf("index = %d\n", vfmts.index);
printf("%s\n", vfmts.description);
3、设置摄像头相关格式,相关格式可以在第二部打印出来,我这里设置的是MJPEG格式,大家根据自己摄像头来设置。
//3、设置摄像头格式
struct v4l2_format v4format;
v4format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
v4format.fmt.pix.height = 480;
v4format.fmt.pix.width = 640;
v4format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //根据自己的摄像头支持格式
int ret = ioctl(fd, VIDIOC_S_FMT ,&v4format);
if(ret < 0)
{
perror("set format fail!");
}
printf("set camrea format MJPEG succesful!\n");
4、申请内核缓冲区队列映射到用户空间,也就是向内核申请一个位置用于存放数据
struct v4l2_requestbuffers requstbuffer;
requstbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
requstbuffer.count = 4; //申请4个缓冲区
requstbuffer.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_REQBUFS, &requstbuffer);
if(ret < 0)
{
perror("request fail!");
}
5、把内核的缓冲区映射到用户空间
//unsigned char *mptr[4]; //保存映射后用户空间的首地址 ,非常重要,以后采集就是在这里,我这里设置为全局变量了;
unsigned int size[4];
struct v4l2_buffer mapbuffer;
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
int i = 0;
for(i=0; i < 4; i++)
{
mapbuffer.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer);
if(ret < 0)
{
perror("查询内核队列空间失败");
}
mptr[i] =(unsigned char *) mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset); //映射
size[i] = mapbuffer.length;
//通知内核使用完成
ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer);
if(ret < 0)
{
perror("back fail!");
}
}
6、开始采集,VIDIOC_STREAMON(开始采集写数据到队列中)
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if(ret < 0)
{
perror("start camrea fail");
}
7、提取数据,并且将数据转为Mat格式。
//从队列中提取一帧数据
//struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);
if(ret < 0)
{
perror("read fail");
}
/*将mjpg格式数转化成opencv Mat格式*/
std::vector<uchar> mjpeg_vec((char *)mptr[readbuffer.index], (char *)mptr[readbuffer.index] + readbuffer.bytesused);
cv::Mat frame = cv::imdecode(mjpeg_vec, cv::IMREAD_COLOR);
show(frame); //我这里封装了一个显示函数
//告诉内核我已经使用完毕
ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
if(ret < 0 )
{
perror("back fail");
}
8、停止采集、释放空间
//停止采集,不再向队列写数据
ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
for(i=0 ; i< 4; i++)
{
munmap(mptr[i], size[i]);
}
close(fd);
二、利用封装好的函数进行显示
这个函数是我自己封装好的函数,用于显示OpenCV的Mat格式。底层还是用到了Linux通用的架构
struct framebuffer_info {
uint32_t bits_per_pixel;
uint32_t xres_virtual;
};
struct framebuffer_info get_framebuffer_info(const char* framebuffer_device_path)
{
struct framebuffer_info info;
struct fb_var_screeninfo screen_info;
int fd = -1;
fd = open(framebuffer_device_path, O_RDWR);
if (fd >= 0) {
if (!ioctl(fd, FBIOGET_VSCREENINFO, &screen_info)) {
info.xres_virtual = screen_info.xres_virtual;
info.bits_per_pixel = screen_info.bits_per_pixel;
}
}
return info;
};
void show(const cv::Mat& image) {
framebuffer_info fb_info = get_framebuffer_info("/dev/fb0");
std::ofstream ofs("/dev/fb0");
if (image.depth() != CV_8U) {
std::cerr << "Not 8 bits per pixel and channel." << std::endl;
return;
}
else if (image.channels() != 3) {
std::cerr << "Not 3 channels." << std::endl;
return;
}
else{
cv::Mat transposed_image;
cv::resize(image, transposed_image, cv::Size(DISPLAY_X, DISPLAY_Y));
int framebuffer_width = fb_info.xres_virtual;
int framebuffer_depth = fb_info.bits_per_pixel;
cv::Size2f image_size = transposed_image.size();
cv::Mat framebuffer_compat;
switch (framebuffer_depth) {
case 16:
cv::cvtColor(transposed_image, framebuffer_compat, cv::COLOR_BGR2BGR565);
for (int y = 0; y < image_size.height; y++) {
ofs.seekp(y * framebuffer_width * 2);
ofs.write(reinterpret_cast<char*>(framebuffer_compat.ptr(y)), image_size.width * 2);
}
break;
case 32: {
std::vector<cv::Mat> split_bgr;
cv::split(transposed_image, split_bgr);
split_bgr.push_back(cv::Mat(image_size, CV_8UC1, cv::Scalar(255)));
cv::merge(split_bgr, framebuffer_compat);
for (int y = 0; y < image_size.height; y++) {
ofs.seekp(y * framebuffer_width * 4);
ofs.write(reinterpret_cast<char*>(framebuffer_compat.ptr(y)), image_size.width * 4);
}
} break;
default:
std::cerr << "Unsupported depth of framebuffer." << std::endl;
break;
}
}
}
三、利用按键输入来进行拍照
这个底层逻辑还是文件的读写,因为底层驱动都已经写好了。不过这里我使用了多线程进行实现拍照功能,主线程用于显示,这个线程用于获取按键输入和保存图片。
static void *thread_takephoto_control(void *args)
{
/* 打开文件 */
if (0 > (fd_key = open("/dev/input/event2", O_RDONLY))) {
perror("open error");
exit(-1);
}
int i = 0;
while(1)
{
char file[100];
/* 循环读取数据 */
if (sizeof(struct input_event) != read(fd_key, &in_ev, sizeof(struct input_event))) {
perror("read error");
exit(-1);
}
if (EV_KEY == in_ev.type) { //按键事件
switch (in_ev.value) {
case 1:
/*将mjpg格式数转化成opencv Mat格式*/
std::vector<uchar> mjpeg_vec((char *)mptr[readbuffer.index], (char *)mptr[readbuffer.index] + readbuffer.bytesused);
frame = cv::imdecode(mjpeg_vec, cv::IMREAD_COLOR);
sprintf(file,"%u.jpg",i);
cv::imwrite(file, frame);
printf("拍照成功!%s已保存至当前目录\n", file);
i++;
break;;
}
}
}
close(fd_key);
}
这里使用了全局变量,mptr[]。也就是前面我们申请的用户空间。
四、实物展示进行拍照
1、在板端进行运行
2、摄像头拍下的照片
3、LCD屏幕上进行显示
需要完整代码可以私信我。