跳转至

USB摄像头拍照Demo

本章节将讲解如何D1-H上使用一个USB摄像头拍摄一张照片。

D1-H哪吒开发板上有一个USB Host接口(即电脑上那种插鼠标键盘的USB口),同时D1-H Tina Linux支持UVC(USB Video Class,USB视频类),这样D1-H就具备了开发和使用USB摄像头的软硬件条件。

前期准备

硬件准备

  • USB免驱摄像头一个,标准USB摄像头均可,淘宝直接搜“USB摄像头”搜出来排名靠前的随便买一个就行,本文中调试用到的是一个海康威视的摄像头,零售价格大概数十元。

  • 哪吒开发板一块

软件准备

  • 在运行本例之前,请确保你的Hello Word 的过程没有出现问题。即交叉编译工具和ADB工具都可正常使用或已经添加进环境变量。详见上一章节:

编译第一个程序:Hello Word

下文将详细介绍demo的源码写的内容。

硬件连接

主要连接串口调试,USB连接电脑可以用来传输数据和供电,USB摄像头连接到开发板的USB接口。

IMG_20210716_101433

连接好的系统就是下图的样子:

IMG_20210716_101433

如果此时你的开发板是开机的话,终端会打印USB摄像头连接的Log。如下图:

A8986C52-B9A5-4277-BA34-F57B50BF8601

从图中可以看到,摄像头为 HIK 720P 的摄像头,同时摄像头挂在 USB1总线、为 input3 设备。

此时我们查看/dev外设目录,可以发现有/dev/video0/dev/video1设备,video1 为 video0的映射。

226F4615-D579-4909-98C6-D4501BF3AC48

到此,我们的哪吒开发板已经成功连接上了USB摄像头,下一步是写程序来使用它。

程序获取

在编写程序之前,我们需要了解一下Linux中摄像头的接口标准。在LInux系统中,摄像头之所以能被识别离不开我们的系统对摄像头的驱动支持。

Video4Linux2(Video for Linux Two, 简称V4L2)是Linux中关于视频设备的驱动框架,为上层访问底层的视频设备提供统一接口。V4L2主要支持三类设备:视频输入输出设备、VBI设备和Radio设备,分别会在/dev目录下产生videoX、vbiX和radioX设备节点,其中X是0,1,2等的数字。如USB摄像头是我们常见的视频输入设备。

Linux 中强大的第三方库如:FFmpeg和OpenCV对V4L2均支持。

本例就使用V4L2库完成摄像头对图片的捕捉,并将其保存为一张图片。

依照Tina SKD开发架构,我们的代码创建在prckage目录下,我们新建文件夹camerademo在文件夹中新建test.c,或将提供资源中的代码包中的test.c拷贝至此处。

cd d1-tina-open/package/test
mkdir camerademo
cd camerdemo
touch test.c

程序编译

程序编译采用最简单的命令行编译程序:

riscv64-unknown-linux-gnu-gcc test.c -o test

编译后,我们当前目录就会生成可执行文件test

文件烧录及传输

我们在windows中使用ADB工具将其送入开发板中:

adb push test ./.

8845ACBA-B861-4348-ADC8-AEFE13A5486A

程序运行

文件传输成功后,我们从开发板中运行。

运行之前首先要赋予文件可执行权限,然后再运行。

chmod +x test
./test

运行截图:

62287F93-8BAC-45b8-8567-4943A2BC4CDC

此时程序会打印保存成功的Log。我们使用Ctrl+C终止程序运行后,可在当前文件夹看到有1.jpeg图片生成,我们将他拿出来查看。

依然使用ADB

adb pull /root/1.jpeg .

运行截图:

F1186D7D-31AE-42b1-A36F-266CBECB4A5B

打开1.jpeg可以看到刚才拍摄到的图片:

12DE8FDE-C484-4850-8E1D-224E97EB8691

如果可以成果看到拍摄的图片,那么恭喜你,你已经给D1-H哪吒添上了一双眼睛,以后你就可以用这双眼睛,去探索这个有趣的世界了!

进阶:程序代码注释及讲解

开头说过我们Linux使用的是V4L2框架获取的摄像头数据。该框架的使用流程如下:

  1. 打开设备

  2. 初始化设备

  3. 注册内存映射I/O

  4. 开始捕捉

  5. 停止捕捉

  6. 关闭设备

根据如上大纲,我们分布讲解。

1. 打开设备

打开设备使用C标准接口open函数,返回文件(设备)描述符。打开文件的方式可以选择可读可写方式(O_RDWR)无阻塞方式(O_NONBLOCK)打开

#define CAM_DEV "/dev/video0"
int cam_fd;
if((cam_fd = open(CAM_DEV,O_RDWR)) == -1)
{
    perror("ERROR opening V4L interface.");
    return -1;
}

2. 初始化设备

初始化设备调用init_device函数。

(1) 调用ioctl函数,对设备的I/O通道进行管理,查询设备能力,并将结果保存在结构体v4l2_capability中,结构体v4l2_capability内容如下:

struct v4l2_capability {
    __u8    driver[16]; // name of the driver module (e.g. "bttv")
    __u8    card[32]; // name of the card (e.g. "Hauppauge WinTV")
    __u8    bus_info[32]; // name of the bus (e.g. "PCI:" + pci_name(pci_dev) )
    __u32   version; // KERNEL_VERSION
    __u32   capabilities; // capabilities of the physical device as a whole
    __u32   device_caps; // capabilities accessed via this particular device (node)
    __u32   reserved[3]; // reserved fields for future extensions
};

if(ioctl(cam_fd,VIDIOC_QUERYCAP,&cam_cap) == -1)
    {
        perror("Error opening device %s: unable to query device.");
        return -1;
    }

(2) 判断是否视频捕获设备V4L2_CAP_VIDEO_CAPTURE

if((cam_cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0) 
    {
        perror("ERROR video capture not supported.");
        return -1;
    }

(3) 调用ioctl函数,设置流数据格式,包括宽、高、像素格式,并将结果保存在结构体v4l2_format中,结构体v4l2_format

struct v4l2_pix_format {
    __u32           width;
    __u32           height;
    __u32           pixelformat;
    __u32           field;      /* enum v4l2_field */
    __u32           bytesperline;   /* for padding, zero if unused */
    __u32           sizeimage;
    __u32           colorspace; /* enum v4l2_colorspace */
    __u32           priv;       /* private data, depends on pixelformat */
};

struct v4l2_format { // stream data format
    __u32    type; // enum v4l2_buf_type; type of the data stream
    union {
        struct v4l2_pix_format      pix;     // V4L2_BUF_TYPE_VIDEO_CAPTURE, definition of an image format
        struct v4l2_pix_format_mplane   pix_mp;  // V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, definition of a multiplanar image format
        struct v4l2_window      win;     // V4L2_BUF_TYPE_VIDEO_OVERLAY, definition of an overlaid image
        struct v4l2_vbi_format      vbi;     // V4L2_BUF_TYPE_VBI_CAPTURE, raw VBI capture or output parameters
        struct v4l2_sliced_vbi_format   sliced;  // V4L2_BUF_TYPE_SLICED_VBI_CAPTURE, sliced VBI capture or output parameters
        __u8    raw_data[200];                   // user-defined, placeholder for future extensions and custom formats
    } fmt;
};

主要设置长宽及出输出格式。

    struct v4l2_format v4l2_fmt;
    v4l2_fmt.type = V4L2_CAP_VIDEO_CAPTURE;
    v4l2_fmt.fmt.pix.width = WIDTH;
    v4l2_fmt.fmt.pix.height = HEIGHT;
    v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
    if (ioctl (cam_fd, VIDIOC_S_FMT, &v4l2_fmt) == -1) 
    {   
        perror("ERROR camera VIDIOC_S_FMT Failed.");
        return -1;
    }

3. 注册内存映射I/O

因为io采用的是MMAP即内存映射方式,因此调用init_mmap函数:

(1) 调用ioctl函数,设置内存映射I/O,并将结果保存在结构体v4l2_requestbuffers中,结构体v4l2_requestbuffers内容如下:

enum v4l2_memory {
    V4L2_MEMORY_MMAP             = 1,
    V4L2_MEMORY_USERPTR          = 2,
    V4L2_MEMORY_OVERLAY          = 3,
    V4L2_MEMORY_DMABUF           = 4,
};

struct v4l2_requestbuffers {
    __u32           count;
    __u32           type;       /* enum v4l2_buf_type */
    __u32           memory;     /* enum v4l2_memory */
    __u32           reserved[2];
};
struct v4l2_requestbuffers v4l2_req;
    v4l2_req.count = NB_BUFFER;
    v4l2_req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    v4l2_req.memory = V4L2_MEMORY_MMAP;
    if (ioctl (cam_fd, VIDIOC_REQBUFS, &v4l2_req) == -1) 
    {
        perror("ERROR camera VIDIOC_REQBUFS Failed.");
        return -1;
    } 

(2) 调用ioctl函数,查询缓冲区状态,并将结果保存在结构体v4l2_buffer中.

(3) 调用mmap函数,应用程序通过内存映射将帧缓冲区地址映射到用户空间;通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能。

struct v4l2_buffer v4l2_buf;
    v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    v4l2_buf.memory = V4L2_MEMORY_MMAP;
      for(i = 0; i < NB_BUFFER; i++) 
   {
        v4l2_buf.index = i;
        if(ioctl(cam_fd, VIDIOC_QUERYBUF, &v4l2_buf) < 0)
        {
            perror("Unable to query buffer.");
            return -1;
        }

        pic.tmpbuffer[i] = (unsigned char*)mmap(NULL, v4l2_buf.length, PROT_READ, MAP_SHARED, cam_fd, v4l2_buf.m.offset);
        if(pic.tmpbuffer[i] == MAP_FAILED)
        {
             perror("Unable to map buffer.");
             return -1;
        }
        if(ioctl(cam_fd, VIDIOC_QBUF, &v4l2_buf) < 0)
        {
            perror("Unable to queue buffer.");
            return -1;
        }
   }

4. 开始捕捉

调用ioctl函数,VIDIOC_QBUF,并将结果保存在结构体v4l2_buffer中;

struct v4l2_buffer buff;
    buff.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buff.memory = V4L2_MEMORY_MMAP;
    if(ioctl(cam_fd, VIDIOC_DQBUF, &buff) < 0)
    {
        printf("camera VIDIOC_DQBUF Failed.\n");
        usleep(1000*1000);
        return -1;
    }

5. 停止捕捉

调用munmap函数,取消映射设备内存;

 for(i=0; i<NB_BUFFER; i++)
  munmap(pic[0].tmpbuffer[i],pic[0].tmpbytesused[i]);

6. 关闭设备

调用close函数关闭设备。

close(cam_fd);