![eb3f967b49355da547ae6bbb2c26ae53.png](https://img-blog.csdnimg.cn/img_convert/eb3f967b49355da547ae6bbb2c26ae53.png)
文章比较长,请选择性阅读!
AnChangNice/oled_display_guigithub.com![0454119ca7ed40616a4acd333dc6d6ff.png](https://img-blog.csdnimg.cn/img_convert/0454119ca7ed40616a4acd333dc6d6ff.png)
我的B站视频链接,进空间有更多有趣视频:
OLED GUI Demo | 上位机工具_哔哩哔哩 (゜-゜)つロ 干杯~-bilibiliwww.bilibili.com![46ed976eeffa20dd9a4771e48eda5568.png](https://img-blog.csdnimg.cn/img_convert/46ed976eeffa20dd9a4771e48eda5568.png)
Demo视频(初级版本):
![6222c493b6055a881778e3ed4536702b.png](https://img-blog.csdnimg.cn/img_convert/6222c493b6055a881778e3ed4536702b.png)
简介
看到B站各种屏幕播放bad apple非常有趣。真是八仙过海各显神通,bad apple俨然成了“炫技”的不二之选(虽然我也不认为有什么技术含量),我也来凑一凑热闹吧。当然看过我之前作品的应该了解我,别人做过的我就懒得做了,要做就来个与众不同的。
看过不少badapple的作品,但是所展示的要么帧率不高,要么操作非常繁琐(除了作者本人基本也没人会用,或愿意去用)。而我要做的是既要易用,又要有一定的姿势水平。
- 易用:要易用GUI是少不了的,而且为了方便传播使用了Python、PyQt5
- 扩展性:要适用多种尺寸的显示屏、各种扫描方式等、方便拓展到其他平台
- 功能:图片、视频、屏幕投影都要有
- 流畅:至少24fps
- 显示效果好:除基本的阈值二值化,还有抖动、误差扩散算法加持
最终呈现的效果如下:
![1f0c0cf5e9c8e2d8c87f36e2aaf10a20.png](https://img-blog.csdnimg.cn/img_convert/1f0c0cf5e9c8e2d8c87f36e2aaf10a20.png)
![ef48f5bf726cf8b01278e17190d70649.png](https://img-blog.csdnimg.cn/img_convert/ef48f5bf726cf8b01278e17190d70649.png)
![9477edbae621c08a33d7882e0ca5e8bc.png](https://img-blog.csdnimg.cn/img_convert/9477edbae621c08a33d7882e0ca5e8bc.png)
![00ba36f4a44bb23648a86c9519a6ae55.png](https://img-blog.csdnimg.cn/img_convert/00ba36f4a44bb23648a86c9519a6ae55.png)
首先根据我看到的情况,播放bad apple有几大技术流:
- PC 显示器流,这种就是各种花式:MATLAB、Python、LabVIEW、C、C++、C#、Java等语言实现以PC显示器为输出的方式,不涉及硬件的最为简单常见的形式。最为常见的:Terminal字符画。
- PC声卡流,主要显示方式为示波器、显像管,使用XY扫描方式将经调制的图像信号显示在示波器上。
- MPU流,很多运行Linux其实速度与资源与PC没啥区别了。
- MCU流,以Arduino、ESP32等开源硬件为主的,以及以STM32等为主的采用TF卡存储图像,而后在TFT、OLED、LED点阵、甚至是机械翻转点阵屏(Flip-dot display)、以及各种非主流的交互显示器。
- FPGA数电流,TF卡,RAM,通过VGA接口输出图像到显示器。
其中1、3、5显示图像可以是RGB、GRAY、BW比较自由;2受限于硬件条件也不做讨论。我们仅看那些资源有限的MCU在只能显示明暗两种色调的显示屏上的情况(彩色图像的数据量对MCU来说过于庞大)。
其次我们看看它们主要的二值化方式:
- 阈值二值化,99%
- 抖动(dithering),我也只见过一个
- 误差扩散(error-diffusion),做图像处理方面的人可能还知道,玩MCU的基本没人知道
系统设计
要放bad apple那么就要了解整个流程:
首先将输入的图片、视频帧、屏幕截图转换为灰度图像,为了提高图像的对比度使用直方图均衡,之后缩放到显示屏的尺寸;灰度图像经二值化算法转换为黑白图像,之后使用与显示屏幕扫描方式相同的方式对图像进行采样,放进一维字节数组;最后,通过串口将一维数组发送到MCU。
![e71cdf18216fe7881a8148e292b9d3d8.png](https://img-blog.csdnimg.cn/img_convert/e71cdf18216fe7881a8148e292b9d3d8.png)
MCU使用UART的DMA接收(使得接收操作不消耗MCU时间),DMA接收的数据放在其中一个缓存里;MCU使用IIC/SPI将另一个接收完成的缓存数据写入OLED的GRAM中,至此,OLED即可显示出正确的图像。
![402901761d1fc8f99e8b8ce9576649df.png](https://img-blog.csdnimg.cn/img_convert/402901761d1fc8f99e8b8ce9576649df.png)
下面我们着重分析一下二值化算法的效果对比,而后是屏幕采样(也有说是取模)。
二值化算法:
二值化算法可以说是整个项目的核心之一了,显示效果如何全靠它。通常,做黑白屏幕显示图片等都是使用阈值法直接二值化,这样简单粗暴的方法对于某些场景确实比较有效;如bad apple本身就及其接近黑白图像,选用二值化就能得到很好的效果。但是这种情况比较少,大部分的图片以彩色与灰度为主,彩色图可以转换为灰度图,所以可以一并讨论。
灰度图明暗变化细节丰富,如果采用一刀切的阈值二值化显然是不合适的。半色调技术生来就是用于将灰度图像转换为黑白图像,你的打印机、书本、报纸上的图像全靠它才能输出灰度图像,如果你使用放大镜仔细观察就能够发现:灰度图像是由疏密不同的黑点组成的。同样地,我们可以使用半色调技术在黑白屏上模拟灰度图像的效果。
我们这里使用了两种半色调技术:抖动法、误差扩散。下面是它们的效果对比:
![e3eeec2e81c6cb5be0673998fb5410aa.png](https://img-blog.csdnimg.cn/img_convert/e3eeec2e81c6cb5be0673998fb5410aa.png)
当你遮住右边的图像,只看阈值图像,你很难看出图中的机器人,而抖动与误差扩散较好地保留了明暗的细节;将抖动与误差扩散进行比较,你能发现抖动图像中有较为明显的网格状条纹;在误差扩散中你也能看到一些朝向右下的条纹,这些是仅采用自左向右的扩散方式造成的,采用往复扩散可以解决这个问题;误差扩散的效果无疑是最好的。
关于抖动的细节可以看这篇文章:抖动算法小议
CSDN-专业IT技术社区-登录
而误差扩散参见:
https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering
取模原理:
有了理想的黑白图像,但是此时的图像并不能直接被显示屏使用,我们还需要把它转换为显示屏能够直接使用的数据,这个过程被称为取模(我在上面说是采样来着)。而要了解取模到底做了什么,我们就必须知道GRAM中数据与显示屏的像素点之间的对应关系。
这里我们有一个32×16像素的显示屏,下图中每四个绿色格子与四个蓝色格子组成的一行/列为一个byte。扫描方式1为横向(byte),自左向右,逐行自上而下进行扫描。扫描方式2为横向(byte),自上而下,逐列自左向右进行扫描。扫描方式3为纵向(byte),自上而下,逐列自左向右进行扫描。扫描方式4为纵向(byte),自左向右,逐行自上而下进行扫描。扫描到的顺序即为它们在GRAM的存储顺序(可以理解为写入GRAM的顺序),而特殊的自右向左,自下而上的扫描方式可以理解为对图像进行水平、垂直翻转后再进行扫描的情况。
![19641fc5aec35d2d6413a411ab8d6fb4.png](https://img-blog.csdnimg.cn/img_convert/19641fc5aec35d2d6413a411ab8d6fb4.png)
仅仅知道扫描方式还是不够,我们还必须知道一个字节内数据的存储方式。如下图所示,一个字节内各个bit有MSB与LSB之分。MSB为高bit在前,LSB为低bit在前。若以蓝色为1、绿色为0,则MSB的数值为0xF0、LSB的数值为0x0F,这个就是他们的区别。
![5da3439d4514a894e05a2ac8c063e3f1.png](https://img-blog.csdnimg.cn/img_convert/5da3439d4514a894e05a2ac8c063e3f1.png)
一般显示屏的扫描方式是可选的,但是MSB/LSB是不可选的,并且byte的朝向也是不可选的。通过阅读显示屏的数据手册我们即可获取这些信息。
通过以上扫描方式以及MSB、LSB信息,我们就可以使用其规则按照扫描到的顺序依次将一个平面图像的数据转换为一维字节数组,这个过程就是取模。
屏幕的一些小知识:
这里我们希望速度尽可能地快,因此OLED的极限刷新速率对我们来说非常重要。OLED主要有两个瓶颈:首先是接口速率,它决定了你能够以多快的速率将数据从MCU写入GRAM(通常是IIC、SPI、并口等);其次是GRAM的一帧数据完整刷新到屏幕的速率(单位fps)。而最终能实现的最大速率由最小的那个决定(木桶效应)。通常情况下,我们可以通过查阅数据手册获得屏幕的刷新速率公式,其参数总是与系统时钟相关。系统时钟通常由RC震荡电路构成,由于IC内电阻通常精度不高,所以数据手册给出的时钟参数通常仅具有参考意义(别当真)。因此测量屏幕的真实刷新速率既关键,难度又非常高,我这里埋个坑,后续再出篇文讲述一下如何测量屏幕的实际刷新速率,这是一个非常有脑洞与挑战性的事情,因此独立出去非常有必要。
我们知道了取模的过程,接下来就来算一算:这里才用的OLED为128*64分辨率的单色屏幕,每个像素点仅需1bit;那么这样一帧图像的数据仅128*64/8=1024bytes=1KB,若是用1M波特率的串口传输,每个byte需要10bits(8bits数据位+1bit起始位+1bit停止位),那么实际上传输速率为100KB/s;也就是说我们的传输速率是100fps。
若分辨率为320*240,此时1M波特率传输速率仅13fps;再若屏幕为16位色的,那么传输一帧需要1.5s;那么需要多大的RAM呢?150KB(已经秒掉一堆MCU,使用外部RAM的除外);显然串口不适合处理这样的数据量,那么若是用USB-FS呢?12Mb/s,大概9fps,USB-HS呢?480Mb/s,390fps。很显然若要使用彩屏,USB-HS才是你的选择(支持USB-HS也很少),而这也暂时超出了我的技能范围,所以我才不会考虑彩屏。
硬件组成
本文所采用的硬件设备如下图所示:
- MCU:STM32F103C8T6
- OLED:128*64 ( SSD1306、IIC)
- 通讯接口:USB-UART
- 上位机:PC
![05bd659517a8db1c6788e69485099de5.png](https://img-blog.csdnimg.cn/img_convert/05bd659517a8db1c6788e69485099de5.png)
![aed95b4375db103caf849cb08374c0af.png](https://img-blog.csdnimg.cn/img_convert/aed95b4375db103caf849cb08374c0af.png)
接线图:
![63db34acc0b545c77ec17b5b6e6a4198.png](https://img-blog.csdnimg.cn/img_convert/63db34acc0b545c77ec17b5b6e6a4198.png)
四、 软件
当你完全理解了上文并且具有C、STM32、Python的基础,想实现自己实现一个完全可以,无非是花费些时间罢了。而对于不具备上述基础的同学,我也无需多费口舌(^.^),你直接用我做好的,这个也是我做这个小项目的初衷——让更多人能够把玩badapple,这里我说些关键的点。(实现细节感兴趣的还是看源码吧)
PC上位机软件使用Python、PyQt、OpenCV等技术实现,Python负责具体的流程控制,PyQt负责GUI交互,OpenCV负责对图像、视频进行操作。除此之外便是核心的二值化与取模的代码,由于这部分运算量很大,Python的性能就能不能够满足实时性的需求了,所以使用了C编写的动态链接库来处理大量的运算。但是早期的代码是使用Python实现的,你可以在Git上看采用dll之前的代码。除此之外,图像处理部分采用了单独的线程来避免GUI等待,使得交互更为流畅。由于使用多个线程,处理的不太好,经常会有卡死的情况。还有吐槽一下Qt的Slider,跟踪模式下太快了,导致我的处理速度跟不上经常卡住,如果能够设置触发间隔就会好很多。
由于每帧数据的长度是相等的,我们可以按照固定的长度进行接收,数据帧的传输没有使用校验等机制,实际上MCU也没有时间去做这些。传输的每帧数据都是图片数据,MCU收到后直接写入GRAM即可。为了简化设计,具体的屏幕尺寸、通讯波特率需要使用者自行把握,建议使用1000’000Hz或更高。
五、 总结
这个小项目做的还算比较完整,虽然不完美,同样的也不能够满足所有人的需求(实际上也不可能)。但是我注重的是它有别于你能看到的绝大多数badapple项目,既有一定的深度,又具有一些观赏性。
当然实际上的困难远比上面说到的困难,断断续续的做了很久。在此之前我对于PyQt的了解仅限于数小时的网络视频课程,即使是现在也仅仅是能够实现一些简单的需求而已,不得不感叹一个人的精力是有限的,尤其对于我这样比较笨的人来说。
最后如果我的这个小项目能够对你有所帮助,那就太好了。