目录

汪沫远的个人博客

一直爱着从痛苦荒芜中生出的喜悦

X

使用U8G2在oled屏幕上显示胡桃摇动画

概述

使用的是ESP32驱动128x64分辨率的oled屏幕,gif图片如下,使用的是b站up主海螺张的视频和作品,如果侵权请联系我,点击此处查看gif来源
在这里插入图片描述

图像处理

为什么要处理?

我的oled屏幕只能显示两种像素,即黑与白,由于绘制函数的限制,只能一张张位图开始绘制。目前没有找到较好的取模软件,码一个算了,由于本人嗜好,采用java + opencv处理。大致要分为以下步骤(不想看这部分硬啃图像取模的可以直接往后翻,有取模好的数据和代码):

  1. 导入opencv
  2. 分解gif图
  3. 灰度化
  4. 裁剪大小及缩放
  5. 二值化前预处理
  6. 二值化
  7. 按照驱动进行取值
  8. 将数据输出为头文件

导入opencv

java导入opencv,我用的版本是3.4,导入步骤如下(已经配置好的可忽略):

  1. 去opencv官网下载安装好opencv
  2. 将build/java目录下的opencv-3414.jar导入项目
  3. 在程序执行前加载动态链接库,加载哪个由系统位数决定,我这加载的是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();
}

最后的话

  1. 关于效果图严重闪屏的问题:其实是视频拍出来效果变差了,肉眼看效果挺好的
  2. 外壳是同学自己建模3d打印的,电路板也是同一大佬画的
  3. 开发环境platformio+clion,采用esp32的板子
  4. 仅供学习使用,要代码的直接找我就行,外壳和电路板得问问同学同不同意放出来

资源下载

Gif.h


标题:使用U8G2在oled屏幕上显示胡桃摇动画
作者:汪沫远
地址:https://blog.wangzetong.online/articles/2024/08/24/1724495586302.html