第三章 软件设计
3.1 软件系统组成
本设计的软件系统可分为视频采集模块、数据传输模块、JPEG图像解压缩模块、视频显示模块、人机交互界面模块。各模块的功能分别为:
(1)视频采集模块。获取摄像头参数及设置成像参,使生成的图像大小与接收端的显示屏幕相匹配。
(2)视频数据传输模块。此模块用来通过UDP多播协议发送和接收视频数据。
(3)JPEG图像解压模块。把JPEG图像解压获取像素数组。
(4)视频图像显示模块。把像素数组写入帧缓冲来实现视频的显示。
(5)人机交互界面模块。此模块是用来实现用户登录和菜单选择功能。
3.2 发送端程序设计
发送端程序要实现的功能为视频采集和视频数据发送。为了检测视频的网络延迟效果,本设计在发送端也加入了视频显示功能。下面以这三个功能模块的设计实现来逐一讲述。
3.2.1 采集视频数据
在Linux系统下,我们是通过Linux内核内置的Video4linux2(简称V4L2)驱动来访问底层的视频设备的。Linux有一重要哲学就是任何东西都是文件,所以视频设备也是设备文件,我们可以像访问普通文件一样对其进行读写,该文件为/dev/video3。当摄像头每次采集到一帧图像后,就把图像数据写到/dev/video3这个文件里。我们可以通过系统自带的API(应用程序接口)来读出摄像头文件里的数据,这些函数已声明在
操作流程图如图3-1所示,数据交互图如图3-2所示。
下面为该操作流程的关键函数解析。
(1)打开设备文件,得到设备文件的文件描述符fd
int fd;
fd = open("/dev/video3",O_RDWR);
参数解析:
/dev/video3:视频设备文件名
O_RDWR:可读可写
fd: opend成功反返回文件描述符
(2)查询设备支持哪种格式,把支持的格式打印出来
truct v4l2_fmtdesc fmt; //查询设备格式所用结构体
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_ENUM_FMT,&fmt);// 查询设备支持哪种格式
参数解析:
fd:视频设备文件描述符
VIDIOC_ENUM_FMT,:查询设备支持格式的宏
fmt: struct v4l2_fmtdesc类型的结构体
printf("format:%s\n",fmt.description);//成功可打印视频所支持的格式
(3)设置视频格式
struct v4l2_format s_fmt;
s_fmt.fmt.pix.width = 640;//宽设置为640像素
s_fmt.fmt.pix.height = 480;//高设置为480像素
s_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_S_FMT,&s_fmt);// 设置视频格式
参数解析:
fd: 视频设备文件描述符
VIDIOC_S_FMT:设置视频格式的宏
s_fmt:视频格式结构体变量
(4)申请内核态缓冲reqbuf
struct v4l2_requestbuffers reqbuf;
reqbuf.count = 1;设置缓冲空间数为1
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
ioctl(fd,VIDIOC_REQBUFS,&reqbuf); 申请内核态缓冲
参数解析:
fd: 视频设备文件描述符
VIDIOC_REQBUFS:申请内核态缓冲的宏
&requbuf:内核缓冲的结构体变量
(5)查询内核缓冲
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
ioctl(fd,VIDIOC_QUERYBUF,&buf);
参数解析:
fd: 视频设备文件描述符
VIDIOC_QUERYBUF:查询内核缓冲的宏
&requbuf:保存内核缓冲状态的结构体变量
(6)把内核空间分配好的缓冲映射到用户空间
ub.len = buf.length;
ub.start = mmap(NULL,buf.length,//映射长度
PROT_READ|PROT_WRITE,
MAP_SHARED,
fd,buf.m.offset);
(7)添加到采集队列
ioctl(fd,VIDIOC_QBUF,&buf);
(8)启动视频采集
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_STREAMON,&type);
(9)监测视频采集是否完成,采用多路复用的方法
int select(intnfds, //最大文件描述符加1
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
structtimeval *timeout);
(10)从队列中取出缓冲
ioctl(fd,VIDIOC_DQBUF,&buf);
(11)处理图像
process_image(ub.start,ub.len);
(12)停止/再次采集
ioctl(fd,VIDIOC_STREAMOFF,&type);/ioctl(fd,VIDIOC_QBUF,&buf);
(13)资源回收
munmap(ub.start,ub.len);
close(fd);[13]
图3-1 采集视频数据流程图
图3-2 视频图像采集数据交互图
由上面的操作流程中可看出摄像头拍摄到的每一帧图像数据到保存在以ub.start为首地址的内存空间中,大小为ub.len。我们只需要一旦监测到视频采集完成就把该空间的数据提取出来或通过网络发送到接收端或显示出来。
3.2.2 JPEG
JPEG 是Joint Photographic Experts Group(联合图像专家小组)的缩写,是第一个国际图像压缩标准[14]。JPEG图像压缩算法不仅能够在提供良好的压缩性能的,而且具有比较好的重建质量,因而被广泛应用于图像、视频处理领域。
JPEG本身只是把一个图像转换为RGB分量的字节数据串流(streaming),而并没有说明这些字节被储存在哪个硬件上。.jpeg/.jpg图像文件格式就是采用JPEG压缩标准的图片格式,该格式其实是一种有损压缩格式,但是由于JPEG压缩技术十分先进,他能在较高的压缩率的情况下压缩图片,也能展现十分逼真的图像,因此实用JPEG压缩技术能很好的节约磁盘空间。由于本设计采用的USB摄像头它采集到的图像数据直接是JPEG格式,所以怎样把JPEG图片在屏幕上显示出来是本设计重点研究问题。当每一帧图片动态显示时就成了视频。
JPEG格式图片本身是一种压缩过的图片格式,所以我们要显示JPEG图片必须要先把JPEG解压缩,最终得到只有RGB三原色分量的图像数据,在通过帧缓冲设备把图像显示出来。
在Linux系统下,我们可以利用libjpeg库里面的提供的API解压jpeg文件,其操作流程图如图3-3所示。
下面为该操作流程的关键函数解析
(1)分配并初始化一个jpeg解压对象。
structjpeg_decompress_structdinfo; //定义了一个jpeg的解压对象
structjpeg_error_mgrjerr; //定义一个错误变量
dinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&dinfo); //初始化这个解压对象
(2)指定要解压缩的图像文件。
FILE *infile;
infile = fopen("xxx.jpg", "r");//infile为jpeg文件的文件描述符
jpeg_stdio_src(&dinfo, infile); //为解压对象dinfo指定要解压的文件
(3)调用jpeg_read_header()获取图像信息。
jpeg_read_header(&dinfo, TRUE);
(4)设置jpeg解压缩对象dinfo的一些参数,可采用默认参数,即不进行设置。
(5)调用jpeg_start_decompress()函数用来启动解压过程。
jpeg_start_decompress(&dinfo);
调用jpeg_start_decompress(&dinfo)函数之后,JPEG解压对象dinfo中下面这几个成员变量将会比较有用:
output_width: 图像的输出宽度
output_height: 图像的输出高度
output_component: 每个像素的分量数,也即字节数,3/4字节
在调用jpeg_start_decompress(&dinfo); 之后往往需要为解压后的扫描线上的所有像素点分配存储空间,
output_width * output_components(一行的字节数,output_height行)
(6)读取一行或者多行扫描线数据并进行处理。
//output_scanline表示扫描的总行数
//output_height表示图像的总行数
while (dinfo.output_scanline
{
jpeg_read_scanlines(&dinfo, &buffer, 1);
}
读入到存储空间中,紧接着是第二个扫描线,最后是图像的底边的扫描线被读入存储空间中。
(7)调用 jpeg_finish_decompress()完成解压过程。
jpeg_finish_decompress(&dinfo);
(8)调用jpeg_destroy_decompress()释放jpeg解压对象dinfo。
jpeg_destroy_decompress(&dinfo);[16]
图3-3 JPEG图像解压流程图
从以上操作步骤可以看出JEPG文件的解压是一行一样的解压的,每解压一行就保存在缓冲区buffer里。所以我们可以每解压一行就显示一行来实现图像的显示。
但是解压出来的图像数据到底如何在LCD屏幕上显示呢?这其实跟LCD的显示原理有关。LCD屏幕由y行且每行x个像素点的矩阵组成;在屏幕上显示图像,就是给每个像素点显示一种颜色。而每个颜色值都可以由红绿蓝三种颜色构成,我们把红绿蓝三种颜色量化,这样就可以表示不同的颜色了。公式可表示为:
Color =x[R] +y[G] +z[B] (3-1)
所以我们可以在内存中( 显存)开辟一段空间,用来保存屏幕上像素点的颜色值,然后操作屏幕就直接操作这段内存就可以了,这就是我们说的帧缓冲(frame buffer)。实现方法就是把屏幕设备文件/dev/fbo映射到一个内存空间plcd,把JPEG解压后的数据写入plcd空间,就可在屏幕上显示JPEG图片。其流程图如下:
图3-4 视频显示流程图
下面为该操作流程的关键函数解析
(1) 打开屏幕设备,获得该设备文件描述符
int fb;
fb = open("/dev/fb0", O_RDWR);
(2) 获取屏幕信息
structfb_var_screeninfofbinfo;
ioctl(fb, FBIOGET_VSCREENINFO, &fbinfo);//这步可以省略
(3) 把帧缓冲空间映射到以plcd指针为首地址的内存空间
plcd = mmap(NULL, fbinfo.xres * fbinfo.yres *(fbinfo.bits_per_pixel/8), PROT_WRITE, MAP_SHARED, fb, 0);
(4) 把JEPG解压后的数据写入plcd内存里去
int pixels = 800 * 480 ;//本设计分辨率为800*480
int i, j = 0;
char *p = plcd;
for (i = 0; i < pixels; i++)
{
*p++ = gImage_jpeg[j++]; //B
*p++ = gImage_jpeg[j++];//G
*p++ = gImage_jpeg[j++];//R
}
(5)解除映射
munmap(plcd,);
(6)关闭屏幕设备
close(fb);[17]
3.2.3 UDP
多播(也称多址广播或组播)技术,是一种可以用一台或多台主机发送单一数据包到多台主机的TCP/IP网络技术[18]。由于多播属于一点对多点的通信技术,可以有效节省网络带宽。本设计因为要实现总公司和分公司同时实现对车间的视频监控,所以采用的是UDP多播协议。
多播通信必须使用IP多播地址,在IPv4中它属于D类IP地址,其范围为224.0.0.0到239.255.255.255。
在Linux系统下,我们可以利用socket网络套接字及其相关的函数接口来实现网络的发送或接收数据。其所有的接口函数声明都在
(1) SOCK_STREAM: 流式套接字
主要针对传输层协议为TCP
(2) SOCK_DGRAM: 数据报套接字
主要针对传输层协议为UDP
(3) SOCK_RAW: 原始套接字
本设计使用的是UDP多播协议,所以应设置socket类型为SOCK_DGRAM(数据报套接字)类型。下面为用socket套接字实现多播的发送和接收数据的流程图:
图3-5 多播协议实现流程图
下面为该操作流程的关键函数解析:
(1 ) 创建一个数据报套接字
int sock;
sock = socket(AF_INET, SOCK_DGRAM, 0);//创建数据报套接字
(2) 绑定地址
structsockaddr_inmultiAddr;
memset(&multiAddr,0, sizeof(multiAddr));
multiAddr.sin_family = AF_INET;
multiAddr.sin_port = htons(5678);//设置端口号
inet_aton("224.10.10.1", &(multiAddr.sin_addr));//绑定地址,这里假设为224.10.10.1
(3) 发送或接收数据
发送数据,即把数组buf里的数据发送到多播网络中去
sendto(sock, buf, strlen(buf), 0 , (structsockaddr*)&multiAddr, sizeof(multiAddr));
接收数据即把多播发来的数据接收到buf数组里去
recvfrom(sock, buf, 1024, 0, (structsockaddr*)&remoteAddr, &addrLen);
3.3
发送端程序包含数据接收模块、视频显示模块和人机交互界面模块。数据接收模块和视频显示模块前面已经讲述过了,所以下面重点讲述人机交互界面模块的设计实现。本设计的人机交互界面都是用Qt完成的。
3.3.1
用户登录界面就是使用户通过点击触控屏实现帐号与密码的输入,然后把帐号的信息在数据库里索引来匹配对应的密码。如果帐号密码匹配正确则进入功能菜单界面,否则提示错误。该模块的两大重难点是:1.数字键盘的设计2.数据库的建立与索引。
数字键盘的设计的重要核心是把数字按钮把字符数组相关联起来。首先我们先定义两个字符数组,分别为帐号字符数组usr_id和密码字符数组passwd;还有两个整形常量,分别为帐号数组下标index_id和密码数组下标index_psw。最后定义一个输入模式标志位flag_mode,我们定义当flag_mode为0时,数字按钮与帐号字符数组相关联,flag_mode为1时,数字按钮与密码字符数组向关联。我们通过点击Tab按钮来改变flag_mode的值和改变旁边的指示标签的显示或隐藏状态。
变量声明部分:
char usr_id[32];//帐号字符数组
char passwd[32];//密码数组
intindex_id;//帐号数组下标
intindex_pwd;//密码数组下标
intflag_mode;//输入模式标志位
当输入模式为0时,我们按下数字按钮时,帐号字符数组的第一个元素的值就是该按钮对应的字符,同时帐号数组下标index_id自加。依此类推,实现帐号的输入。当帐号输入完成后,我们按下Tab按钮,此时输入模式就转换成1,我们按下数字按钮时,密码字符数组的第一个元素的值就是该按钮对应的字符,同时密码数组下标index_id自加。同理,以次类推就可以实现密码的输入。最后把账号数组的内容在用户ID编辑栏显示出来,密码数组的内容在密码编辑栏显示出来(密码显示模式)。
数字键盘的实现流程图为图3-6所示。
在我们输入完帐号和密码后,点击登录按钮,这时系统应该把帐号和密码在数据库里索引,查询其结果是否匹配。如果匹配成功就能进入功能菜单界面了。其数据库的具体实现过程,我们在3.3.3节再详细讨论。
(a)
(b)
图3-6 数字键盘实现流程图
图3-7 用户登录界面
3.3.2
当我们要进行注册用户或删除视频资料时,这是我们需要用户拥有更高的权限,这就是用户管理权限。该功能的实现是通过数字键盘输入数字,然后在数据库调用ID为000的记录,此ID为固定保存管理员权限密码的记录,不可被注册为用户。最后与该记录的密码核对。如相匹配则获得管理员权限,就可以进行重置管理员权限密码、删除录像等操作,否则就提示密码错误。此功能关于用到数据库的实现,在3.3.3节细讲。
图3-8 获取管理员权限密码流程图
图3-9 获取管理员权限界面
3.3.3
用户注册界面其形式跟用户登录界面差不多,只是多了一行密码确认编辑栏。此模块的重点是实现数据库的创建和记录的添加。
所谓数据库,其实就是一个存放数据可以快速索引的电子数据仓库。它实现了数据的集中控制,能通过某种算法在庞大的数据库中按照指定条件快速索引出相匹配的记录。
Qt软件是用C++语言编写程序的,它提供了一套数据库接口,其接口函数的声明都包含在
(1) CREATE TABLE 创建一个表
语法:
CREATE TABLE "表名"
(
"列名1" 数据类型 [限制],
"列名2" 数据类型 [限制],
...
);
(2) SELECT 语句用于从表中选取一个或多个数据,其结果存储在一个结果表中 语法:
SELECT 列名1, 列名, ……, 列名n FROM 表名 [ WHERE 列名 运算符 值 AND/OR 列名;
(3) INSERT INTO 向数据库表中插入数据(一个或多条记录)
语法:
INSERT INTO 表名 VALUES(value1, value2, ...);
(4) UPDATE 更新数据库表中的数据
语法:
UPDATE 表名 SET 列名1 = 新值,列名2 = 新值, ……, 列名n = 新值 WHERE 列名 运算符 值 ;
(5) DELETE 从数据库表中删除数据(一条或多条记录)
语法:
DELETE FROM 表名 WHERE 列名 运算符 值;
其操作流程图如下
:
图3-10 数据库实现流程图
图3-11 用户注册界面
下面为关键函数解析:
(1) 创建数据库
sqldb db;
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("your.db");
(2) 打开数据库
bool ok = db.open();
QSqlQueryquery(db);
(3) 执行SQL语句
query.exec("create table if not exists user(name text,passwordtext,data text)");//创建数据库
query.exec("select * from user");//查询记录
(4) 访问回调函数
while(query.next())
{
QString name = query.value(0).toString();//查询名字
int password = query.value(1).toInt();//查询密码
QString data = query.value(0).toString();//查询数据
}
从上面的操作流程可知,我们首先要创建一个数据库和表,表有两列,分别为帐号和密码。后面我们在注册用户时,要先在表里索引是否帐号已存在,如果不存在,在添加一条新的记录。