天天看點

嵌入式linux之在lcd上顯示攝像頭圖像

前言

在學習本章的實驗後,可以實作的功能是:在嵌入式闆卡上跑linux系統,通過對裝置接口的操作實作将攝像頭的資料解碼運輸到lcd上,使得lcd上面實時顯示攝像頭采集的圖像。

在學習本章之前需要有一定的基礎才更好地了解學習,建議大家先翻閱前面的兩篇文章:

使用攝像頭采集圖檔

使用攝像頭采集圖像并顯示在pc上

攝像頭的資料采集格式

目前主流的用于開發的攝像頭,如ov5640、ov2640、ov7725等,基本都帶有采集YUV格式圖像的功能,但是采集jpg等格式大多不支援。我們經常接觸到的色彩空間的概念,主要是RGB , YUV這兩種(實際上,這兩種體系包含了許多種不同的顔色表達方式和模型,如sRGB, Adobe RGB, YUV422, YUV420 …), RGB如前所述就是按三基色加光系統的原理來描述顔色,而YUV則是按照 亮度,色差的原理來描述顔色。然而,lcd是使用rgb格式圖檔才能顯示的,所有在這裡我将介紹怎麼将YUV格式的資料轉換成rgb格式然後顯示在lcd上。

首先介紹一下什麼是YUV,拿一個像素點來說明。Y表示這像素點的亮度,而U和V分别代表色彩的分量。其實YUV格式下又分幾種格式,我這裡來說說YUYV 4:2:2格式,一般都是支援這種格式的。

這裡的比例意思是一個像素點中,Y:U:V=4:2:2。也就是說亮度占的比例是其他的兩倍,每一幀資料的排列是碼流Y0 U0 Y1 V1 Y2 U2 Y3 V3 。但是這樣不行,完整的一個像素點需要有U和V,那怎麼辦呢?那就要複制隔壁像素點的U或者V過來,這樣一個像素點就湊齊了YUV了。于是補全之後就成了YUYV像素[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]。到了這一步,就可以将每個像素點轉化成RGB像素了。下面是轉換公司,因為YUV格式是比rgb占用記憶體小的,用四個個位元組表示兩個像素點而rgb888是使用3個位元組表示一個像素點。轉換公式是固定的,不了解的話直接套用這個函數就可以了。

void yuyv_to_rgb(unsigned char *yuyvdata, unsigned char *rgbdata, int w, int h)
{
	//碼流Y0 U0 Y1 V1 Y2 U2 Y3 V3 --》YUYV像素[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]--》RGB像素
	int r1, g1, b1; 
	int r2, g2, b2;
	for(int i=0; i<w*h/2; i++)
	{
	    char data[4];
	    memcpy(data, yuyvdata+i*4, 4);
	    unsigned char Y0=data[0];
	    unsigned char U0=data[1];
	    unsigned char Y1=data[2];
	    unsigned char V1=data[3]; 
		//Y0U0Y1V1  -->[Y0 U0 V1] [Y1 U0 V1]
	    r1 = Y0+1.4075*(V1-128); if(r1>255)r1=255; if(r1<0)r1=0;
	    g1 =Y0- 0.3455 * (U0-128) - 0.7169*(V1-128); if(g1>255)g1=255; if(g1<0)g1=0;
	    b1 = Y0 + 1.779 * (U0-128);  if(b1>255)b1=255; if(b1<0)b1=0;
	 
	    r2 = Y1+1.4075*(V1-128);if(r2>255)r2=255; if(r2<0)r2=0;
	    g2 = Y1- 0.3455 * (U0-128) - 0.7169*(V1-128); if(g2>255)g2=255; if(g2<0)g2=0;
	    b2 = Y1 + 1.779 * (U0-128);  if(b2>255)b2=255; if(b2<0)b2=0;
	    
	    rgbdata[i*6+0]=r1;
	    rgbdata[i*6+1]=g1;
	    rgbdata[i*6+2]=b1;
	    rgbdata[i*6+3]=r2;
	    rgbdata[i*6+4]=g2;
	    rgbdata[i*6+5]=b2;
	}
}

           

實作代碼講解

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>

#include <linux/fb.h>
#include <stdio.h>


int fd_fb;                                                        /* 檔案句柄 */
static struct fb_var_screeninfo var; /* LCD可變參數 */
static unsigned int *fb_base = NULL;                        /* Framebuffer映射基位址 */
static int screen_size;                                      /* 整個Framebuffer大小*/
int lcd_w = 800 ,lcd_h= 480;

//解碼函數
void yuyv_to_rgb(unsigned char *yuyvdata, unsigned char *rgbdata, int w, int h)
{
	//碼流Y0 U0 Y1 V1 Y2 U2 Y3 V3 --》YUYV像素[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]--》RGB像素
	int r1, g1, b1; 
	int r2, g2, b2;
	for(int i=0; i<w*h/2; i++)
	{
	    char data[4];
	    memcpy(data, yuyvdata+i*4, 4);
	    unsigned char Y0=data[0];
	    unsigned char U0=data[1];
	    unsigned char Y1=data[2];
	    unsigned char V1=data[3]; 
		//Y0U0Y1V1  -->[Y0 U0 V1] [Y1 U0 V1]
	    r1 = Y0+1.4075*(V1-128); if(r1>255)r1=255; if(r1<0)r1=0;
	    g1 =Y0- 0.3455 * (U0-128) - 0.7169*(V1-128); if(g1>255)g1=255; if(g1<0)g1=0;
	    b1 = Y0 + 1.779 * (U0-128);  if(b1>255)b1=255; if(b1<0)b1=0;
	 
	    r2 = Y1+1.4075*(V1-128);if(r2>255)r2=255; if(r2<0)r2=0;
	    g2 = Y1- 0.3455 * (U0-128) - 0.7169*(V1-128); if(g2>255)g2=255; if(g2<0)g2=0;
	    b2 = Y1 + 1.779 * (U0-128);  if(b2>255)b2=255; if(b2<0)b2=0;
	    
	    rgbdata[i*6+0]=r1;
	    rgbdata[i*6+1]=g1;
	    rgbdata[i*6+2]=b1;
	    rgbdata[i*6+3]=r2;
	    rgbdata[i*6+4]=g2;
	    rgbdata[i*6+5]=b2;
	}
}

void lcd_show_rgb(unsigned char *rgbdata, int w ,int h)
{
    unsigned int *ptr = fb_base;  //不要直接對lcd基位址操作以免卡住
    for(int i = 0; i <h; i++) {
        for(int j = 0; j < w; j++) {
                memcpy(ptr+j,rgbdata+j*3,4);//rgb用3個位元組表示一個像素點
        }
        ptr += lcd_w;
        rgbdata += w*3;
    }
}

int main(void) 
{
    fd_fb =  open("/dev/fb0", O_RDWR); //打開LCD檔案
    if(fd_fb < 0)
   {
      perror("/dev/fb0");
      exit(-1);
   }
   if (ioctl(fd_fb,FBIOGET_VSCREENINFO,&var))  //讀取lcd參數
   {
      printf("can't get fb_var_screeninfo \n");
      goto err1;
   }

   printf("X:%d  Y:%d  bbp:%d\n",var.xres,var.yres,var.bits_per_pixel);

   screen_size = var.xres *var.yres *var.bits_per_pixel /8;  //整個Framebuffer大小,bits_per_pixel 表示色深,除以8表示轉換機關位元組
   //建立記憶體映射 友善控制
   fb_base = (unsigned int*)mmap(NULL,screen_size,PROT_READ|PROT_WRITE,MAP_SHARED, fd_fb,0);
   if(fb_base == NULL)
   {
      printf("can't mmap Framebuffer\n");
      goto err1;
   }

    int fd = open("/dev/video1",O_RDWR); //打開攝像頭節點,請根據自己的攝像頭所在節點修改
    if (fd < 0)
    {
        perror("打開裝置失敗");
        return -1;
    }

    //擷取攝像頭支援格式 ioctl(檔案描述符,指令,與指令對應的結構體)
    struct v4l2_format vfmt;

    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //攝像頭采集
    vfmt.fmt.pix.width = 640; //設定攝像頭采集參數,不可以任意設定
    vfmt.fmt.pix.height = 480;
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //設定視訊采集格式 ,根據上一步測得
    
    int ret = ioctl(fd,VIDIOC_S_FMT,&vfmt); //寫入攝像頭參數
    if (ret < 0)
    {
        perror("設定格式失敗1");
    }

    struct v4l2_streamparm Stream_Parm; //定義結構體設定攝像頭幀率
    memset(&Stream_Parm, 0, sizeof(struct v4l2_streamparm));
    Stream_Parm.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;

    Stream_Parm.parm.capture.timeperframe.denominator = 15 ;  //分母
    Stream_Parm.parm.capture.timeperframe.numerator = 1 ;  //分子
    

    ret = ioctl(fd,VIDIOC_S_PARM,&Stream_Parm); //寫入攝像頭幀率
    if (ret < 0)
    {
        perror("設定幀率失敗");
    }

//以下函數是讀取目前攝像頭幀率
/*    Stream_Parm.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ret = ioctl(fd,VIDIOC_G_PARM,&Stream_Parm);
     if (ret < 0)
    {
        perror("擷取幀率失敗");
    }

    printf("Frame rate: %u/%u\n",Stream_Parm.parm.capture.timeperframe.numerator,Stream_Parm.parm.capture.timeperframe.denominator);
*/
    //申請核心空間
    struct v4l2_requestbuffers reqbuffer;
    reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuffer.count = 4; //申請4個緩沖區
    reqbuffer.memory = V4L2_MEMORY_MMAP;  //映射方式

    ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffer);
    if (ret < 0)
    {
        perror("申請空間失敗");
    }
   
    //映射
    unsigned char *mptr[4];//儲存映射後使用者空間的首位址
    unsigned int size[4];
    struct v4l2_buffer mapbuffer;
    //初始化type和index
    mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    for(int 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("放回失敗");
        }
    }
    //開始采集
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ret = ioctl(fd,VIDIOC_STREAMON,&type); 
    if (ret < 0)
        {
            perror("開啟失敗");
        }


    //定義一個空間存儲解碼後的rgb
    unsigned char rgbdata[640*480*3];
    while(1)
    {
        //從隊列中提取一幀資料
        struct v4l2_buffer readbuffer;
        readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //每個結構體都需要設定type為這個參賽要記住
        ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer); 
        if (ret < 0)
            {
                perror("讀取資料失敗");
            }
        
    //顯示在lcd上      
        yuyv_to_rgb(mptr[readbuffer.index],rgbdata,640,480);//把jpeg資料解碼為rgb資料
        lcd_show_rgb(rgbdata,640,480);
    
        //通知核心使用完畢
    ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
    if(ret < 0)
        {
            perror("放回隊列失敗");
        }
    }
    //停止采集
    ret = ioctl(fd,VIDIOC_STREAMOFF,&type);

    //釋放映射
    for(int i=0; i<4; i)munmap(mptr[i], size[i]);

    close(fd); //關閉檔案
    return 0;

err1:
   close(fd_fb);
   return -1;
}

           

實驗結果

嵌入式linux之在lcd上顯示攝像頭圖像

使用交叉編譯器編譯沒有問題,使用nfs功能傳輸到開發闆運作。

嵌入式linux之在lcd上顯示攝像頭圖像

看得出,圖像其實還是偏藍的,這和圖像的色彩矯正有關,感興趣的同學可以看看如下的文章:

圖像處理原理

繼續閱讀