使用U8G2在oled屏幕上显示胡桃摇动画
概述
使用的是ESP32驱动128x64分辨率的oled屏幕,gif图片如下,使用的是b站up主海螺张的视频和作品,如果侵权请联系我,点击此处查看gif来源,
图像处理
为什么要处理?
我的oled屏幕只能显示两种像素,即黑与白,由于绘制函数的限制,只能一张张位图开始绘制。目前没有找到较好的取模软件,码一个算了,由于本人嗜好,采用java + opencv处理。大致要分为以下步骤(不想看这部分硬啃图像取模的可以直接往后翻,有取模好的数据和代码):
- 导入opencv
- 分解gif图
- 灰度化
- 裁剪大小及缩放
- 二值化前预处理
- 二值化
- 按照驱动进行取值
- 将数据输出为头文件
导入opencv
java导入opencv,我用的版本是3.4,导入步骤如下(已经配置好的可忽略):
- 去opencv官网下载安装好opencv
- 将build/java目录下的opencv-3414.jar导入项目
- 在程序执行前加载动态链接库,加载哪个由系统位数决定,我这加载的是64位的:
System.load("D:\\OpenCv\\opencv\\build\\java\\x64\\opencv_java3414.dll");
上面的路径是opencv安装路径,因为我电脑64位,加载x64文件夹下的dll
因为本文侧重不在这,所以如果觉得我说的不详细的可以百度下java使用opencv
分解gif
感谢这篇文章提供的思路,copy了一下随便改了改,会将分解后的gif以png格式存在指定文件夹里
/**
*
* @param originalSource 目标gif
* @param newPath 分解后的文件夹路径
* @return 分解后图片的数目
*/
private static int gifSeparate(String originalSource, String newPath) {
try {
GifDecoder gd = new GifDecoder();
int status = gd.read(new FileInputStream(new File(originalSource)));
if (status != GifDecoder.STATUS_OK) {
return -1;
}
for (int i = 0; i < gd.getFrameCount(); i++) {
BufferedImage frame = gd.getFrame(i);
ImageIO.write(frame, "png", new File(newPath + i + ".png"));
}
return gd.getFrameCount();
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
需要注意的是,以上方法调用了animated-gif-lib依赖,使用gradle添加如下:
implementation 'com.madgag:animated-gif-lib:1.4'
灰度化
灰度化的目的是为了方便后续处理,不得不说opencv确实好用,只需要在读取图片的时候指定参数就可以自动完成转化:
Mat img = imread(path, IMREAD_GRAYSCALE);
裁剪大小和缩放
裁剪
由于分解后的图片是240x240的,要显示在128x64的屏幕上,势必要进行缩放。而且上方和左右有部分空白,考虑到缩放比例较大,会导致图片不清晰,于是先进行部分裁剪,经过本人相当差劲的PS技术,发现裁剪到216x216大小较为合适:
Rect r = new Rect(8, 24, 216, 216);
img = new Mat(img, r);
以上代码将在图像img坐标(8,24)开始裁剪216x216大小的矩形
缩放
缩放就没啥好说的了,屏幕大小128x64限制的死死的,为了图像不失真,就只能缩放到64x64了
resize(img, img, new Size(64, 64));
二值化前预处理
其实对于大多数gif来说,这一步并不是必要的,但是我找到这个gif,它背景是透明的。。。。然后二值化的时候会将背景染成黑的,显示效果一言难尽,染白背景需要以下几步:
提取轮廓
提取图像的轮廓,方便后续步骤:
List<MatOfPoint> contours = new ArrayList<>();
Mat mat = new Mat(input.size(), input.type());
//轮廓提取
findContours(input, contours, mat, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
绘制轮廓内区域为白
Mat temp = new Mat(input.size(), input.type());
//复制输入的目的是为了不改变输入图像
input.copyTo(temp);
drawContours(temp, contours, -1, new Scalar(255), -1);
按照上一步骤绘制的图像染白背景
for (int j = 0; j < input.rows(); j++) {
for (int k = 0; k < input.cols(); k++) {
//轮廓之外,背景设白
if (temp.get(j, k)[0] == 0) {
result.put(j, k, 255.0);
}
}
}
二值化
千辛万苦总算到了这一步,将上述处理好的图像二值化(就是变为只有黑和白没有灰的图像):
threshold(output, output, 150, 255, THRESH_BINARY);
按照驱动进行取值
完成了以上步骤,就需要按照oled规定的方式化为二进制数据了,我们现在得到的图像是大小为64x64的二值图,我们要做的是从左到右,从上到下依次查看每个像素点的值,纯黑记为1,纯白记为0,每8个像素就组成了一个字节,将这一字节倒序(即第一位为最低位,第八位为最高位)化为十六进制数据存于数组中,看一眼代码就明白了
//将每一帧的二进制数据分8位存储
//外层list存放每一帧
//内层list存放每一帧中存放每八个像素点构成的二进制字符串
List<List<String>> data = new ArrayList<>();
for (Mat mat : matList) {
List<String> strings = new ArrayList<>();
StringBuilder builder = new StringBuilder();
int length = 0;
for (int i = 0; i < mat.rows(); i++) {
for (int j = 0; j < mat.cols(); j++) {
if (mat.get(i, j)[0] == 255)
builder.append("0");
else
builder.append("1");
if (length == 7) {
length = -1;
strings.add(bin2hex(builder.toString()));
builder.delete(0, 8);
}
length++;
}
}
data.add(strings);
}
//二进制倒序转化为十六进制
private static String bin2hex(String bin) {
StringBuilder string = new StringBuilder();
//倒序
for (int i = 0; i < bin.length(); i++) {
string.append(bin.charAt(bin.length() - 1 - i));
}
int value = Integer.parseInt(string.toString(), 2);
String result = "0x";
if (Integer.toHexString(value).length() < 2)
result += "0" + Integer.toHexString(value);
else
result += Integer.toHexString(value);
return result;
}
将数据输出为头文件
将十六进制数据存于数组中:
//格式化输出
private static String printData(List<List<String>> data) {
StringBuilder builder = new StringBuilder();
builder.append("#ifndef GIF_H\n" +
"#define GIF_H\n" +
"\n");
//一行最多十六个数据
int maxLen = 16, len = 0;
builder.append("static const int gif_length = " + data.size() + ";\n");
builder.append("\nstatic const unsigned char gif[][" + data.get(0).size() + "] = {\n");
for (List<String> datum : data) {
for (String s : datum) {
builder.append(s).append(", ");
len++;
if (len == maxLen) {
builder.append("\n");
len = 0;
}
}
}
builder.deleteCharAt(builder.length() - 1);
builder.append("\n};\n\n");
builder.append("#endif");
return builder.toString();
}
java完整代码
先晒一张项目结构
完整代码
将分解img目录下的inupt.gif文件,并生成Gif.h文件
import com.madgag.gif.fmsware.GifDecoder;
import org.opencv.core.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.opencv.imgcodecs.Imgcodecs.IMREAD_GRAYSCALE;
import static org.opencv.imgcodecs.Imgcodecs.imread;
import static org.opencv.imgproc.Imgproc.*;
public class Main {
static {
System.load("D:\\OpenCv\\opencv\\build\\java\\x64\\opencv_java3414.dll");
}
public static void main(String[] args) throws IOException {
//感谢海螺张提供的gif,https://space.bilibili.com/2425374/dynamic?spm_id_from=444.42.list.card_avatar.click
int len = gifSeparate("img/input.gif", "img/outputGif/output");
List<Mat> matList = new ArrayList<>();
for (int i = 0; i < len; i++) {
Mat img = imread("img/outputGif/output" + i + ".png", IMREAD_GRAYSCALE);
Rect r = new Rect(8, 24, 216, 216);
img = new Mat(img, r);
resize(img, img, new Size(64, 64));
//预处理
Mat output = dealImg(img);
threshold(output, output, 150, 255, THRESH_BINARY);
matList.add(output);
// imshow("img", output);
// waitKey();
}
//将每一帧的二进制数据分8位存储
List<List<String>> data = new ArrayList<>();
for (Mat mat : matList) {
List<String> strings = new ArrayList<>();
StringBuilder builder = new StringBuilder();
int length = 0;
for (int i = 0; i < mat.rows(); i++) {
for (int j = 0; j < mat.cols(); j++) {
if (mat.get(i, j)[0] == 255)
builder.append("0");
else
builder.append("1");
if (length == 7) {
length = -1;
strings.add(bin2hex(builder.toString()));
builder.delete(0, 8);
}
length++;
}
}
data.add(strings);
}
String s = printData(data);
//写入文件
FileWriter writer = new FileWriter("Gif.h");
writer.write(s);
writer.close();
}
//格式化输出
private static String printData(List<List<String>> data) {
StringBuilder builder = new StringBuilder();
builder.append("#ifndef GIF_H\n" +
"#define GIF_H\n" +
"\n");
//一行最多十六个数据
int maxLen = 16, len = 0;
builder.append("static const int gif_length = " + data.size() + ";\n");
builder.append("\nstatic const unsigned char gif[][" + data.get(0).size() + "] = {\n");
for (List<String> datum : data) {
for (String s : datum) {
builder.append(s).append(", ");
len++;
if (len == maxLen) {
builder.append("\n");
len = 0;
}
}
}
builder.deleteCharAt(builder.length() - 1);
builder.append("\n};\n\n");
builder.append("#endif");
return builder.toString();
}
//二进制倒序转化为十六进制
private static String bin2hex(String bin) {
StringBuilder string = new StringBuilder();
for (int i = 0; i < bin.length(); i++) {
string.append(bin.charAt(bin.length() - 1 - i));
}
int value = Integer.parseInt(string.toString(), 2);
String result = "0x";
if (Integer.toHexString(value).length() < 2)
result += "0" + Integer.toHexString(value);
else
result += Integer.toHexString(value);
return result;
}
//输入灰度图,返回将背景转化为白色的灰度图
private static Mat dealImg(Mat input) {
List<MatOfPoint> contours = new ArrayList<>();
Mat mat = new Mat(input.size(), input.type());
Mat result = new Mat(input.size(), input.type());
//轮廓提取
findContours(input, contours, mat, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
Mat temp = new Mat(input.size(), input.type());
input.copyTo(temp);
input.copyTo(result);
drawContours(temp, contours, -1, new Scalar(255), -1);
for (int j = 0; j < input.rows(); j++) {
for (int k = 0; k < input.cols(); k++) {
//轮廓之外,背景设白
if (temp.get(j, k)[0] == 0) {
result.put(j, k, 255.0);
}
}
}
return result;
}
//感谢:https://blog.csdn.net/qgqc_/article/details/105304397,这里稍稍修改了一下
/**
* @param originalSource 目标gif
* @param newPath 分解后的文件夹路径
* @return 分解后图片的数目
*/
private static int gifSeparate(String originalSource, String newPath) {
try {
GifDecoder gd = new GifDecoder();
int status = gd.read(new FileInputStream(new File(originalSource)));
if (status != GifDecoder.STATUS_OK) {
return -1;
}
for (int i = 0; i < gd.getFrameCount(); i++) {
BufferedImage frame = gd.getFrame(i);
ImageIO.write(frame, "png", new File(newPath + i + ".png"));
}
return gd.getFrameCount();
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
}
写的比较乱,也没打算复用,就这样吧
ESP32驱动OLED部分
关于u8g2
没啥好说的,自行连线,需要注意u8g2的构造器别选错了,不然会出现稀奇古怪的问题,不会选的请参考这篇文章,选错很容易出现显示不全的问题
生成头文件的内容
好了,不想看java代码的可以来看这了,点击此处下载已经取好模的胡桃摇gif:
Gif.h
gif_length 的值为共有多少帧
gif[i]代表第i帧的图像数据
绘制胡桃·!!
有了以上的头文件,我们可以开始绘制了,搞俩函数先:
U8G2_SH1106_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 15, /* data=*/ 13, /* cs=*/ 12, /* dc=*/ 14, /* reset=*/
27);
void init_gui() {
u8g2.begin();
}
void show_gif(int i){
//drawBox目的是防止闪屏,所以绘制一个纯黑的矩形覆盖
u8g2.setDrawColor(0);
u8g2.drawBox(0,0,128,64);
u8g2.setDrawColor(1);
u8g2.drawXBM(32,0,64,64,gif[i]);
u8g2.sendBuffer();
}
开始绘制:
#include "Arduino.h"
#include "Gui.h"
void setup(){
init_gui();
}
void loop(){
for (int i = 0; i < gif_length; ++i){
show_gif(i);
delay(10);
}
}
绘制效果
等等,为什么我的胡堂主和我想象中的不一样?
问题以及改进方法
想了想,明白了取模的时候是白色为0,黑色为1取模的,但是显示在oled屏幕上却是为0为黑色,1为白色。解决方法也简单,反白显示一下就行,稍微改一下show_gif函数:
void show_gif(int i){
u8g2.setDrawColor(1);
u8g2.drawBox(0,0,128,64);
u8g2.setDrawColor(0);
u8g2.drawXBM(32,0,64,64,gif[i]);
u8g2.setDrawColor(1);
u8g2.sendBuffer();
}
setDrawColor是设置画笔颜色,这样就能实现反白了
最终效果
附代码
Gif.h见前,不再重复
Main.cpp
#include "Arduino.h"
#include "Gui.h"
void setup(){
init_gui();
}
void loop(){
for (int i = 0; i < gif_length; ++i){
show_gif(i);
delay(10);
}
}
Gui.h
#ifndef ESP32_GUI_H
#define ESP32_GUI_H
#include "Gif.h"
void init_gui();
void show_gif(int i);
#endif //ESP32_GUI_H
Gui.cpp
#include "U8g2lib.h"
#include "Gui.h"
U8G2_SH1106_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 15, /* data=*/ 13, /* cs=*/ 12, /* dc=*/ 14, /* reset=*/
27);
void init_gui() {
u8g2.begin();
}
void show_gif(int i){
u8g2.setDrawColor(1);
u8g2.drawBox(0,0,128,64);
u8g2.setDrawColor(0);
u8g2.drawXBM(32,0,64,64,gif[i]);
u8g2.setDrawColor(1);
u8g2.sendBuffer();
}
最后的话
- 关于效果图严重闪屏的问题:其实是视频拍出来效果变差了,肉眼看效果挺好的
- 外壳是同学自己建模3d打印的,电路板也是同一大佬画的
- 开发环境platformio+clion,采用esp32的板子
- 仅供学习使用,要代码的直接找我就行,外壳和电路板得问问同学同不同意放出来
资源下载
标题:使用U8G2在oled屏幕上显示胡桃摇动画
作者:汪沫远
地址:https://blog.wangzetong.online/articles/2024/08/24/1724495586302.html