树莓派之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大师代发,拒绝转载,转载需要作者授权
树莓派之OLED12864视频播放—BadApple的更多相关文章
- <<开源硬件创客 15个酷应用玩转树莓派>>
本书共分18章,前3章是本书的基础章节,主要介绍了树莓派的一些基本情况和基本操作,来让读者了解树莓派的前世今生,掌握树莓派基本的使用方法.第4~18章主要介绍15个以树莓派为载体的酷炫应用,大家可以按 ...
- 树莓派播放视频的播放器omxplayer
omxplyer为树莓派量身定做的一款GPU硬件加速的播放器,很好的解决了树莓派cpu计算力不足的缺点.(播放时cpu一定都不烫手) 1.安装方法: CTRL + ALT + T 调出终端命令行输入 ...
- StarRTC , AndroidThings , 树莓派小车,公网环境,视频遥控(一)准备工作
原文地址:http://blog.starrtc.com/?p=48 啥也不说,先来个视频看看效果 视频播放器 00:00 00:54 概述为了体现StarRTC的实时音视频传输能 ...
- 树莓派-基于raspivid实现拍视频
经过上一篇<<树莓派-安装摄像头模块>>之后 想要用摄像头模块拍一段视频的话,可以从命令行运行 raspivid 工具.下面这句命令会按照默认配置(长度5秒,分辨率1920x1 ...
- 树莓派搭建 Google TV
出处:http://my.oschina.net/funnky/blog/142067 树莓派搭建 Google TV 目录:[ - ] Google TV是啥玩意 ? 搭建我们自己的Google T ...
- 树莓派USB存储设备自动挂载并通过脚本实现自动拷贝,自动播放视频,脚本自动升级等功能
需求:首先需要树莓派自动挂载USB设备,然后扫描USB指定目录下文件,将相关文件拷贝至树莓派指定目录,然后通过omxplayer循环播放新拷贝文件视频 1. 树莓派实现USB存储设备自动挂载 树莓派U ...
- 详解树莓派Model B+控制蜂鸣器演奏乐曲
步进电机以及无源蜂鸣器这些都需要脉冲信号才能够驱动,这里将用GPIO的PWM接口驱动无源蜂鸣器弹奏乐曲,本文基于树莓派Mode B+,其他版本树莓派实现时需参照相关资料进行修改! 1 预备知识 1.1 ...
- Python应用03 使用PyQT制作视频播放器
作者:Vamei 出处:http://www.cnblogs.com/vamei 严禁任何形式转载. 最近研究了Python的两个GUI包,Tkinter和PyQT.这两个GUI包的底层分别是Tcl/ ...
- Linux主机上使用交叉编译移植u-boot到树莓派
0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...
随机推荐
- background-position 用法介绍
转自:http://blog.csdn.net/jeamking/article/details/5617088 语法: background-position : length || lengt ...
- 计蒜客 30996.Lpl and Energy-saving Lamps-线段树(区间满足条件最靠左的值) (ACM-ICPC 2018 南京赛区网络预赛 G)
G. Lpl and Energy-saving Lamps 42.07% 1000ms 65536K During tea-drinking, princess, amongst other t ...
- 软件工程中的反面模式(anti-pattern)
软件设计 抽象倒置(Abstraction inversion):不把用户需要的功能直接提供出来,导致他们要用更上层的函数来重复实现 用意不明(Ambiguous viewpoint):给出一个模型( ...
- UOJ Rounds
UOJ Test Round #1 T1:数字比大小的本质是按(长度,字典序)比大小. T2:首先发现单调性,二分答案,用堆模拟,$O(n\log^2 n)$. 第二个log已经没有什么可优化的了,但 ...
- 洛谷 P3041 [USACO12JAN] Video Game Combos
题目描述 Bessie is playing a video game! In the game, the three letters 'A', 'B', and 'C' are the only v ...
- [CF930E]/[CF944G]Coins Exhibition
[CF930E]/[CF944G]Coins Exhibition 题目地址: CF930E/CF944G 博客地址: [CF930E]/[CF944G]Coins Exhibition - skyl ...
- Spring Boot中使用MyBatis注解配置详解
传参方式 下面通过几种不同传参方式来实现前文中实现的插入操作. 使用@Param 在之前的整合示例中我们已经使用了这种最简单的传参方式,如下: @Insert("INSERT INTO US ...
- Ionic2 常见问题及解决方案
前言 Ionic是目前较为流行的Hybird App解决方案,在Ionic开发过程中会遇到很多常见的开发问题,本文尝试对这些问题给出解决方案. 一些常识与技巧 list 有延迟,可以在ion-cont ...
- iOS开发中几种常见的存储方式
1.archive 归档 数据的保存 1: let result = NSKeyedArchiver.archiveRootObject(contacts, toFile: path as Strin ...
- 重写规则为什么要options +followsymlinks
重写规则为什么要options +followsymlinks Web服务器的Apache安装编译成功mod_rewrite编译成模块,但当我在网站根目录下放了一个.htaccess配置文件,却得到了 ...