View on GitHub

Yoy-wiw

A website on lisp.

Download this project as a .zip file Download this project as a tar.gz file

用Lisp获取摄像头图像,保存成BMP图片

用Lisp已经有一段时间了,玩点啥呢?忽然看到笔记本有个摄像头,何不抓出个图看看。

22 Nov 2013

设备驱动

我用Fedora Linux系统,一直以为摄像头的驱动没有,鼓捣了一番,发现,驱动好好的,不用像Windows那样要手动按驱动。

 1 [root@linklook _posts]# lsusb
 2 Bus 001 Device 003: ID 058f:a014 Alcor Micro Corp. Asus Integrated Webcam
 3 ......(others)
 4 [root@linklook _posts]# lsusb -d 058f:a014 -v
 5 Bus 001 Device 003: ID 058f:a014 Alcor Micro Corp. Asus Integrated Webcam
 6 Device Descriptor:
 7   bLength                18
 8   bDescriptorType         1
 9   bcdUSB               2.00
10   bDeviceClass          239 Miscellaneous Device
11   bDeviceSubClass         2 ?
12   bDeviceProtocol         1 Interface Association
13   bMaxPacketSize0        64
14   idVendor           0x058f Alcor Micro Corp.
15   idProduct          0xa014 Asus Integrated Webcam
16   bcdDevice            0.03
17   iManufacturer           3 04G6200086K1AN236001YWJ
18   iProduct                1 ASUS USB2.0 WebCam
19   iSerial                 0 
20   bNumConfigurations      1
21 ......(others)

原来内置的摄像头也是WebCam设备,既然驱动已经有了,那就好办了,可以直接使用设备了。Linux关于WebCam的设备的驱动是uvcvideo模块,Linux的设备都是文件,在/dev/目录下:

1 [root@linklook _posts]# lsmod | grep uvc
2 uvcvideo               81006  0 
3 videobuf2_vmalloc      13163  1 uvcvideo
4 videobuf2_core         38816  1 uvcvideo
5 videodev              121874  2 uvcvideo,videobuf2_core
6 media                  20444  2 uvcvideo,videodev
7 
8 [root@linklook _posts]# ls -l /dev/video*
9 crw-rw----+ 1 root video 81, 0 Nov 22 07:42 /dev/video0

说明模块已经加载,设备文件已经识别。先用成熟的工具试一下,看摄像头是否工作。Linux下关于WebCam的软件有xawtv, video4linux, cheese, kopete(KDE), mplayer, guvcviewer等,我用Gnome,就用现成的cheese试试把。

1 [weida@linklook _posts]$ cheese
2 [weida@linklook _posts]$ mplayer tv:// -tv driver=v4l2:width=640:height=480:device=/dev/video0 -fps 15 -vf screenshot

出图像了,说明设备和驱动都没有问题,下面开始写程序。

V4L2程序接口

关于Linux的视频输入有一套接口(或框架),叫v4l2(video for linux), 包含一系列的API: Video Capture Interface, Video Output, Video Overlay, Video Output Overlay, Codec等。先把依赖的包装上:

1 [root@linklook _posts]# rpm -qa | grep v4l
2 gtk-v4l-0.4-3.fc18.x86_64
3 libv4l-0.8.8-3.fc18.x86_64
4 v4l-utils-0.8.8-3.fc18.x86_64
5 v4l-utils-devel-tools-0.8.8-3.fc18.x86_64
6 libv4l-0.8.8-3.fc18.i686

API比较多,目前没有深入研究,直接拿个例子来分析,通过分析C代码,发现流程是这样的:

1. open设备
2. 查看capability
3. set 制式,帧格式
4. 申请帧缓冲
5. mmap共享内存映射,VIDIOC_QBUF入队列
6. 开始采集,VIDIOC_STREAMON
7. Loop, VIDIOC_DQBUF出队列,处理该帧,再放入队列
8. 停止采集,VIDIOC_STREAMOFF
9. 关设备

下面该Lisp登场了.

CL-V4L2库

Common Lisp关于v4l接口有一个库,叫cl-v4l2,是基于cffi的封装,直接用quicklisp安装,同时安装的还有cl-gtk2-gtk, bordeaux-threads, cffi.

1 CL-USER> (ql:quickload "cl-v4l2")
2 CL-USER> (ql:quickload "cl-gtk2-gtk")
3 CL-USER> (ql:quickload "bordeaux-threads")
4 CL-USER> (ql:quickload "cffi")

cl-v4l2有个自带的例子example.lisp,虽然有问题,但仍可以使用,例子用到了cl-gtk2-gtk, cl-gtk2-gtkglext, bordeaux-thread. 用两个线程,一个线程用于采集,一个线程用于显示,显示用gtk库搭建的窗口,用glext库填充了内容。example.lisp的文件需要改一改:

需要将gtk-glext库的位置引用进来,且要把cl-gtk2改为cl-gtk2-gtk.

1 ;; $ LD_PRELOAD=/usr/lib64/libv4l/v4l2convert.so sbcl --load example.lisp
2 (push "~/quicklisp/dists/quicklisp/software/cl-gtk2-20120909-git/gtk-glext/" asdf:*central-registry*)
3 (asdf:oos 'asdf:load-op :cl-v4l2)
4 ;;(asdf:oos 'asdf:load-op :cl-gtk2)
5 (asdf:oos 'asdf:load-op :cl-gtk2-gtk)
6 (asdf:oos 'asdf:load-op :cl-gtk2-gtkglext)
7 (asdf:oos 'asdf:load-op :bordeaux-threads)

运行一下,但不一定成功,即使成功,图像也不对。

想了一下,何不把显示相关的内容裁掉,只留采集慢慢研究。

YUV和BMP

设备采集下来的图像流一般是yuv形式的,我的设备是yuv(4:2:2), 而且是YUYV, 如果要保存成图片,一般要转换为RGB形式,转换公式网上都有。

1 (defun yuv2rgb (y u v)
2   (mapcar #'(lambda (x) (ldb (byte 8 0) (round x))) 
3           (list (+ y (* 1.13983 (- v 128)))
4                 (- y (* 0.39465 (- u 128)) (* 0.58060 (- v 128)))
5                 (+ y (* 2.03211 (- u 128))))))

采集下来的一帧按照YUYV是这样排列的:(y0,u,y1,v)(y2,u,y3,v)(...), 每2个y共用一个uv, 每个y代表一个像素(2个像素用4个字节表示), 如果图像的宽是352, 高是288, 那么一帧的大小是352*288*2=202752字节. 如果转换为RGBA, 一个像素用4个字节表示,所以转换后的缓冲区为202752*2=405504.

1 (defvar *camera-data* NIL)
2 (setq *camera-data* (make-array (* 4 *got-height* *got-width*)
3 				    :element-type '(unsigned-byte 8)
4 				    :initial-element #xff))

BMP是最简单的图像格式,包括:bmp文件头(14Bytes), 位图信息头(50Bytes), 调色板(由于用32位真色彩,所以不用调色板)和数据。

define-binary-type和define-binary-class是两个宏, 目的是方便的创建像C语言struct一样的类型或类, 类中的slot能设定占几个字节, 这样就能够方便的写入文件或从文件中读取. 有关内容在《实用Common Lisp编程》的第24章。

 1 (define-binary-type unsigned-integer (bytes bits-per-byte)
 2   (:reader (in)
 3            (loop with value = 0
 4 ;;                for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte
 5                 for low-bit from 0 upto (* bits-per-byte (1- bytes)) by bits-per-byte
 6                 do (setf (ldb (byte bits-per-byte low-bit) value) (read-byte in))
 7                 finally (return value)))
 8   (:writer (out value)
 9 ;;           (loop for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte
10            (loop for low-bit from 0 upto (* bits-per-byte (1- bytes)) by bits-per-byte
11                 do (write-byte (ldb (byte bits-per-byte low-bit) value) out))))
12 
13 (define-binary-type u1 () (unsigned-integer :bytes 1 :bits-per-byte 8))
14 (define-binary-type u2 () (unsigned-integer :bytes 2 :bits-per-byte 8))
15 (define-binary-type u3 () (unsigned-integer :bytes 3 :bits-per-byte 8))
16 (define-binary-type u4 () (unsigned-integer :bytes 4 :bits-per-byte 8))
17 
18 (define-binary-class bmp-file-header ()
19   ((bf-type u2)
20    (bf-size u4)
21    (bf-reserved-1 u2)
22    (bf-reserved-2 u2)
23    (bf-offbits u4)))
24 
25 (define-binary-class bmp-info-header ()
26   ((info-size u4)
27    (info-width u4)
28    (info-height u4)
29    (info-planes u2)
30    (info-bits u2)
31    (info-compression u4)
32    (info-image-size u4)
33    (info-x-resolution u4)
34    (info-y-resolution u4)
35    (info-n-colors u4)
36    (info-important-colors u4)))
37 
38 (defun init-bmp-file-header (bf-obj data-size)
39   (with-slots (bf-type bf-size bf-reserved-1 bf-reserved-2 bf-offbits) bf-obj
40     (setf bf-type #x4d42
41           bf-size (+ 54 data-size)
42           bf-reserved-1 0
43           bf-reserved-2 0
44           bf-offbits 54))
45   bf-obj)
46 
47 (defun init-bmp-info-header-32 (info width height data-size)
48   (with-slots (info-size info-width info-height info-planes info-bits
49                          info-compression info-image-size
50                          info-x-resolution info-y-resolution
51                          info-n-colors info-important-colors) info
52     (setf info-size 40
53           info-width width
54           info-height height
55           info-planes 1
56           info-bits 32
57           info-compression 0
58           info-image-size data-size
59           info-x-resolution 11811
60           info-y-resolution 11811
61           info-n-colors 0
62           info-important-colors 0))
63   info)

所以把采集下来的一帧保存为bmp文件是很容易的:创建一个文件,写入bmp文件头和位图信息头,再把YUYV转换为RGBA,依次写入文件。

 1   (with-open-file (out file-name
 2                      :direction :output
 3                      :element-type '(unsigned-byte 8)
 4                      :if-does-not-exist :create
 5                      :if-exists :supersede)
 6     (let ((bf (init-bmp-file-header (make-instance 'bmp-file-header) (length *camera-data*)))
 7           (info (init-bmp-info-header-32 
 8                  (make-instance 'bmp-info-header) 
 9                  *got-width* *got-height* (length *camera-data*))))
10       (write-object bf out)
11       (write-object info out)
12       (write-sequence *camera-data* out)))

需要注意一点是:bmp的图像是反转的,因此写入像素时,要从后往前写.

 1 ;;buffers is the frame buffer list.
 2 ;;frame is the index of buffers. means this frame is ready.
 3   (multiple-value-bind (buffer address length)
 4       (values-list (nth frame buffers))
 5 	       (declare (ignore buffer))
 6 ;;address is the beginning of frame buffer.
 7 ;;length is the frame buffer length.
 8 (let ((pixels (floor (/ length 4))))
 9       (loop for i fixnum from 0 below pixels do
10            (let* ((y0 (cffi:mem-aref address :uchar (+ (* 4 i) 0)))
11                   (u (cffi:mem-aref address :uchar (+ (* 4 i) 1)))
12                   (y1 (cffi:mem-aref address :uchar (+ (* 4 i) 2)))
13                   (v (cffi:mem-aref address :uchar (+ (* 4 i) 3)))
14                   (pix-1 (yuv2rgb y0 u v))
15                   (pix-2 (yuv2rgb y1 u v)))
16 
17              (setf (aref *camera-data* (+ (* 8 (- pixels i 1)) 0)) (third pix-2)
18                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 1)) (second pix-2)
19                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 2)) (first pix-2)
20                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 3)) #xff
21                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 4)) (third pix-1)
22                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 5)) (second pix-1)
23                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 7)) #xff
24                    (aref *camera-data* (+ (* 8 (- pixels i 1)) 6)) (first pix-1))))))

虽然不太美观,但是能工作了.

下一步准备把采集到的图像传到电视上.