树莓派之OLED12864视频播放—BadApple
概述
本篇教程讲述了使用树莓派驱动OLED12864液晶屏,并在液晶屏上播放动画和视频.
硬件平台
- 树莓派一台—RaspberryPi_2B。
- OLED12864显示屏一块,SPI接口。
软件平台
- wiringPi—开源树莓派GPIO库。
- EasyBMP—开源BMP图片处理库(这个库是用C++编写的,主要为了方便提取BMP图片数据,我已经做好了数据提取的小工具,可以直接拿去用,不过我还是会贴出源代码,不会C++的朋友也不要着急)。
- KMPlayer视频播放器—用于视频逐帧截图。
- BadApple.mp4—用于此次在液晶屏上播放的视频(此视频是纯黑白的,对于只能显示”黑白”色的LED显示屏来说是绝佳的选择)。
效果展示
原理详解
12864液晶显示屏驱动编写
我这里采用的液晶屏是从淘宝上淘的1.3寸的OLED显示屏,像素点大小比平常大家用的那种大块头的12864(玩单片机的朋友都知道的)小很多,效果自然也要好很多。而且OLED是自发光的,不需要背光。如下图:
我用的这个显示屏是7线的,SPI接口,控制器是SH1106,你用的可能跟我的不一样,但是不要紧,只要把这个显示屏驱动起来就行。如果驱动代码不会写,就到网上去找一份51单片机的代码来改一改就好了,我就是这么做的,^_~,所以关于驱动这块我就只简单介绍一下。
我用的是wiripingPi这个库来驱动GPIO,就相当于把树莓派当一块普通的单片机来用,并没有使用Linux下的字符设备驱动框架,怎么简单怎么来。关于这个库的安装与使用大家就自行百度吧。下面这个例子大家已看就应该会用了:
#include <wiringPi.h>//包含头文件
int main(void)
{
wiringPiSetup();//初始化
pinMode(1, OUTPUT);//设置1脚为输出模式
while(1)
{
digitalWrite(1, HIGH);//1脚输出高电平
delay (100);//延时100毫秒(wiringPi库自带的延时函数)
digitalWrite(1, LOW);//1脚输出低电平
delay (100);//延时100毫秒
}
}
既然都能控制GPIO了,那接下来移植驱动不是分分钟的事了。找个51单片机的驱动来改改就完事了。这里我就不过多叙述了。其实我们只要能实现往液晶屏内部发送显示数据和命令的两个函数,一个定位函数,一个初始化函数就够了。其他的事都与硬件无关了。
在WiripingPi的源码目录里有一个12864显示屏的驱动范例代码,我就是在这份代码基础上该的。这份范例代码还提供了点阵字体和显示字符串的函数:
// /wiringPi-5edd177/devLib/lcd128x64.c
// /wiringPi 5edd177/devLib/lcd128x64.h
// /wiringPi-5edd177/devLib/font.h
/*
* sentData:
* Send an data or command byte to the display.
*********************************************************************************
*/
static void sendData (int32 dat, const int32 cmd)
/*
* setCol: setPos:
* Set the column and line addresses
*********************************************************************************
*/
static void setPos(const int32 x, const int32 y)
/*
* lcd128x64update:
* Copy our software version to the real display
*********************************************************************************
*/
void lcd128x64update (void)
/*
* lcd128x64setup:
* Initialise the display and GPIO.
*********************************************************************************
*/
int32 lcd128x64setup (void)
关于液晶绘图的补充说明
众所周知,使用液晶绘图(二维图形)的两个最基本的API就是set_point和get_point,但是有一点需要注意,set_point好说,所有的液晶硬件都支持,或者可以通过软件巧妙地实现,但是有些液晶硬件上是不支持get_point的,我所知道的就有常用的诺基亚5110的屏幕就不支持。但是这也不要紧,如果我们的单片机内存足够大,完全可以通过软件实现这一功能,不管硬件是否支持。
我在程序里开辟了一片内存区域用来当作”显存”,其实就是一个二维数组(一维也可以,不过计算要复杂点)把所有要显示的都先放到这块在程序内部定义的显存里,需要更新画面的时候再调用之前的lcd128x64update()函数把这片显存里的数据刷到显示屏上就行了。这样做就使得对硬件的操作变简单了,我们只需要硬件提供一个把显存内容更新到硬件中去显示的API就可以了,其他的操作完全在软件内实现。
这片显存的大小是128×8=1024个byte。每隔byte对应8个像素点,这和硬件有关,显示屏内部共有1024个8位的寄存器,每个寄存器控制8个像素点的亮和灭,而且这些寄存器是竖着排列的,每一行有128个,每一列有8个(与之对应的每一行有128个点,每一列有8×8=64个点),(主意我这里是为了方便大家理解才这么讲的,至于实际硬件是不是这样我也不敢保证,大家不要被我误解)请看下图:
图中每一个小格代表一个像素点,所以我们把显存数组定义成下面这种形式,以便和硬件一一对应:
// Size
#defineLCD_WIDTH 128
#defineLCD_HEIGHT 8
// Software copy of the framebuffer
static uint8 frameBuffer [LCD_WIDTH][LCD_HEIGHT];
比如如果像素点坐标用(x,y)分别表示列和行,那么(5, 8)就应该位于frameBuffer[5][1]这个显存单元(byte)中的第0个比特位(0-7,从低到高)。如果我们把frameBuffer[5][1] = 0x01 这个值送入到显示屏内部的(5,1)这个地址单元就能点亮(5, 8)这个像素点。如下图:
下面这个更新显示屏画面的函数现在应该能看懂了把:
/*
* lcd128x64update:
* Copy our software version to the real display
*********************************************************************************
*/
void lcd128x64update (void)
{
int32 x = 0, y = 0;
for(y = 0; y < (LCD_HEIGHT); y++)
{
setPos(0, y);
//每往显示屏中写入一次数据,该显示屏的列坐标就会自动向后加1,所以列坐标x不用每次都设置。只需要设置行坐标y。
for(x = 0; x < LCD_WIDTH; x++)
{
sendData(frameBuffer[x][y], OLED_DATA);
}
}
}
如果仅仅实现用液晶屏显示动画,上面所说的内容是不需要关注的,只需要明白怎么样把一幅图像显示到液晶屏上去就行了。但愿各位看官不要觉得我过于啰嗦。
显示一副图像
在上面,我们已经实现了几个硬件相关的API(关于液晶绘图的补充内容与本帖要实现的功能是无关的): 往液晶的控制器内发送指令和数据的API(sendData), 液晶显示定位的API(setPos). 有了这两个API我们就可以把一副图像显示到液晶上了. 具体的做法就是循环的依次把图像上的每一个像素点发送到液晶控制器中去. 参考代码 lcd128x64update
/*
* sentData:
* Send an data or command byte to the display.
*********************************************************************************
*/
static void sendData (int32 dat, const int32 cmd)
{
int32 i;
if(cmd)
{
OLED_DC_Set();
}
else
{
OLED_DC_Clr();
}
OLED_CS_Clr();
for(i = 0; i < 8; i++)
{
OLED_SCLK_Clr();
if(dat & 0x80)
{
OLED_SDIN_Set();
}
else
{
OLED_SDIN_Clr();
}
OLED_SCLK_Set();
dat <<= 1;
}
OLED_CS_Set();
OLED_DC_Set();
}
/*
* setCol: SetLine:
* Set the column and line addresses
*********************************************************************************
*/
static void setPos(const int32 x, const int32 y)
{
sendData(0xb0 + y, OLED_CMD);
sendData(((x & 0xf0) >> 4) | 0x10, OLED_CMD);
sendData((x & 0x0f) | 0x02, OLED_CMD);
}
/*
* lcd128x64update:
* Copy our software version to the real display
*********************************************************************************
*/
void lcd128x64update (void)
{
int32 x = 0, y = 0;
for(y = 0; y < (LCD_HEIGHT); y++)
{
setPos(0, y);
for(x = 0; x < LCD_WIDTH; x++)
{
sendData(frameBuffer[x][y], OLED_DATA);
}
}
}
视频数据制作
视频播放原理
视频播放其实和图片显示一样,只不过视频是由很多张连续的图像组成的,我们把这一张张图像按照一定速度一张一张依次显示出来就成了会动的视频。这一张张的图像叫做视频的帧。
图像数据提取
既然我们已经能显示一副图像了,而视频又是由连续的图像组成的,那用液晶播放视频是不是变得简单了很多。但问题是,我们要从哪里去得到这一张张的图像呢?而且大小必须与我们使用的液晶屏刚好合适。
这里就要用到KMPlayer这款软件了,KMPlayer是一个视频播放器,带有一个小工具,可以实现视频的”逐帧图”,意思就是把视频的每一帧都截图保存成一副图像。具体操作如下:
前缀指的是保存的图像的名字的前缀,这里把名字保存成连续的最大5位的数字,不足五位会在前面补0。这样做是为了方便后面再程序中处理。
图像数据转换
好了,现在我们得到我们想要的数据了,但是现在还不能直接播放,因为他是彩色的,而我们的液晶屏只支持黑白两种颜色,所以要进行转换,关于图像的二值化,网上有很多相关文章,二值化的质量直接影响到显示效果。我这里才用的是大律法求出图像的阈值。
阀值是图像二值化处理中非常重要的一个参数,意思就是:如果灰度图像的像素颜色值大于该阀值就把该点当作黑色,小于该阀值就把改点当作白色。最简单的做法就是把阀值取为127(255的一半),但是这种做法是不科学的,处理后的二值化图像效果也很不理想。关于阀值的选取是一门很深的学问,有很多经典的算法用于选取该阀值,理论我就不做过多描述了,感兴趣的自己趣网络上搜索相关资料,我这里采用的是比较经典的大律法(OTSU)。算法代码来自网络。
图像经过二值化之后,还需要经过最后一步,就是图像数据的提取,我们需要把这些图片中的像素点数据都提取出来,按照要显示的顺序排好,其实就是先把图像的每一个像素点提取出来按顺序排好,然后按顺序提取每一帧图像数据,最后把这些数据保存成二进制文件。使用的时候我们再把这个文件都入到内存中,这样这些数据在内存中就是按顺序排列的,我们只需要依次读出每一帧图像的数据显示出来即可。
如下图:
可能我描述的不是很清楚,大家看源码或许更好理解。
图像提取和转换源码(采用c++编写,用到了EasyBMP库):
#include <iostream>
#include <string>
#include <cmath>
#include <cstdlib>
#include <cstdio>
#include "EasyBMP/EasyBMP.h"
/*
阀值是图像二值化处理中非常重要的一个参数,意思就是:如果灰度图像的像素
颜色值大于该阀值就把该点当作黑色,小于该阀值就把改点当作白色。最简单的
做法就是把阀值取为127(255的一半),但是这种做法是不科学的,处理后的二
值化图像效果也很不理想。关于阀值的选取是一门很深的学问,有很多经典的
算法用于选取该阀值,理论我就不做过多描述了,感兴趣的自己趣网络上搜索
相关资料,我这里采用的是比较经典的大律法(OTSU)。算法代码来自网络。
*/
int findThreshold(BMP frame) //大津法求阈值
{
#define GrayScale 256 //frame灰度级
int width = frame.TellWidth();
int height = frame.TellHeight();
int pixelCount[GrayScale] = {0};
float pixelPro[GrayScale] = {0};
int i, j, pixelSum = width * height, threshold = 0;
int r = 0 , g = 0 , b = 0 , data = 0;
//统计每个灰度级中像素的个数
for(i = 0; i < height; i++)
{
for(j = 0; j < width; j++)
{
r = frame(j, i)->Red;
g = frame(j, i)->Green;
b = frame(j, i)->Blue;
data = pow((pow(r, 2.2) * 0.2973 +
pow(g, 2.2) * 0.6274 +
pow(b, 2.2) * 0.0753), (1 / 2.2));
pixelCount[data]++;
}
}
//计算每个灰度级的像素数目占整幅图像的比例
for(i = 0; i < GrayScale; i++)
{
pixelPro[i] = (float)pixelCount[i] / pixelSum;
}
//遍历灰度级[0,255],寻找合适的threshold
float w0, w1, u0tmp, u1tmp, u0, u1, deltaTmp, deltaMax = 0;
for(i = 0; i < GrayScale; i++)
{
w0 = w1 = u0tmp = u1tmp = u0 = u1 = deltaTmp = 0;
for(j = 0; j < GrayScale; j++)
{
if(j <= i) //背景部分
{
w0 += pixelPro[j];
u0tmp += j * pixelPro[j];
}
else //前景部分
{
w1 += pixelPro[j];
u1tmp += j * pixelPro[j];
}
}
u0 = u0tmp / w0;
u1 = u1tmp / w1;
deltaTmp = (float)(w0 * w1 * pow((u0 – u1), 2)) ;
if(deltaTmp > deltaMax)
{
deltaMax = deltaTmp;
threshold = i;
}
}
return threshold;
}
int main(int argc, char *argv[])
{
int nFrames;
BMP Input;
FILE *fp;
//unsigned int n = 0;
fp = fopen("output.bin", "wb"); /* 输出文件 */
//fp = fopen("output.h", "wb"); /* 输出文件 */
printf("******************************************************************\n");
printf("* 说 明 *\n");
printf("* *\n");
printf("* 本软件用于把24位色的BMP图像帧转换成用于LCD显示的二进制数 *\n");
printf("* 据文件。图片名必须为从00000开始的连续数字,总共5位,不足五位 *\n");
printf("* 要在前面用0补齐(也就是说本软件最多能处理99999张图片!)。如: *\n");
printf("* 00008.bmp。请把本软件和图片文件放在同一目录下! *\n");
printf("******************************************************************\n");
printf(">请输入图片数目: ");
scanf("%d", &nFrames);
printf("\n>转换开始……\n");
char FullPath[255], FileName[20];
for (int Frame = 0; Frame < nFrames; Frame++)
{
strcpy(FileName, "00000.bmp");
FileName[4] += (Frame / 1) % 10;
FileName[3] += (Frame / 10) % 10;
FileName[2] += (Frame / 100) % 10;
FileName[1] += (Frame / 1000) % 10;
FileName[0] += (Frame / 10000) % 10;
/*if(Frame > 9999)
{
strcpy(FileName, "000000.bmp");
FileName[5] += (Frame/1) % 10;
FileName[4] += (Frame/10) % 10;
FileName[3] += (Frame/100) % 10;
FileName[2] += (Frame/1000) % 10;
FileName[1] += (Frame/10000) % 10;
FileName[0] += (Frame/100000) % 10;
}*/
//fputs(FileName, fp);
//fputs("[ ] = \r\n{\r\n", fp);
printf("\r>正在处理: %s", FileName);
strcpy(FullPath, "");
strcat(FullPath, FileName);
if(Input.ReadFromFile(FullPath) == false)
{
printf("\n>打开图片文件出错!\n");
fclose(fp);
fp = NULL;
return –1;
}
int nSeg;
int threshold = findThreshold(Input);//阀值
nSeg = Input.TellHeight() / 8;
for (int iSeg = 0; iSeg < nSeg; iSeg++)
{
for (int x = 0; x < Input.TellWidth(); x++)
{
unsigned char Data;
//char Outdat[5] = {0};
Data = 0x00;
for (int j = 0; j < 8; j++)
{
int y = iSeg * 8 + j;
int r = Input(x, y)->Red;
int g = Input(x, y)->Green;
int b = Input(x, y)->Blue;
/*int brightness = (int)floor(
0.299*Input(x,y)->Red +
0.587*Input(x,y)->Green +
0.114*Input(x,y)->Blue);*/
int brightness = pow((pow(r, 2.2) * 0.2973 +
pow(g, 2.2) * 0.6274 +
pow(b, 2.2) * 0.0753), (1 / 2.2));
if (brightness > 255) brightness = 255;
if (brightness < 0) brightness = 0;
if (brightness > threshold) Data |= (0x01 << j);
}
//sprintf(Outdat,"0x%02X,",Data);
//fputs(Outdat, fp);
//fputs(&Data, fp);
fwrite(&Data, 1, 1, fp);
//n++;
//if(n >= 16)
//{
// n = 0;
// fputs("\r\n", fp);
//}
}
}
//fputs("\r\n};\r\n", fp);
}
printf("\n>转换结束……\n");
fclose(fp);
fp = NULL;
printf("\n>按任意键退出!\n");
getchar();
getchar();
return 0;
}
播放视频
图像数据提取并转换完成之后,就可以直接显示了。这相比上面的内容来说是很简单的,只不过是每次读出128个字节的数据并显示到液晶屏上,等待片刻之后再读取下一帧并显示。等待的时间是根据原视频计算出来的,如果原视频的帧率是24帧每秒,也就是每镇图像显示的时间是1/24秒。
接下来就是见证奇迹的时刻,这么小的屏幕居然也可以播放视频!
下面我直接给出视频显示的源码:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <wiringPi.h>
#include “lcd128x64.h”
#define N 128*8
int main(int argc, char *argv[])
{
int x = 0, y = 0;
lcd128x64setup();
int fd1;
unsigned char buf[N] = {0};
size_t nbyte = 0;
//while(1)
{
if(argc < 2)
{
printf("Please input Movie name,Like this: %s <movie.bin>\n", argv[0]);
return –1;
}
if((fd1 = open(argv[1], O_RDONLY)) < 0)
{
perror("Fail to open Movie file1!\n");
return –1;
}
printf("\nBegin to play movie!\n");
int nb = 0;
int n = 1;
while((nbyte = read(fd1, buf, N)) > 0)
{
//lcd128x64putbmpspeed(0, 0, 128, 64, buf, 0);
lcd128x64putbmpspeed(0, 0, 128, 64, buf, 1);
//lcd128x64update ();
printf("\rFrame %d", n++);
fflush(stdout);
delay(39);
}
printf("\nPlay movie Over!\n");
close(fd1);
}
printf("Exit!\n");
}
项目内文件截图
补充
暂时没有树莓派之OLED12864视频播放—BadApple
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权