/******************** F r i e n d s h i p E n d e r ******************** * 本程序隶属于 Readguy 开源项目, 请尊重开源开发者, 也就是我FriendshipEnder. * 如果有条件请到 extra/artset/reward 中扫描打赏,否则请在 Bilibili 上支持我. * 项目交流QQ群: 926824162 (萌新可以进来问问题的哟) * 郑重声明: 未经授权还请不要商用本开源项目编译出的程序. * * @file ex06_Image.ino * @author FriendshipEnder (f_ender@163.com), Bilibili: FriendshipEnder * @version 1.0 * @date create: 2023-11-01 * last modify: 2023-11-06 * @brief ReadGuy 图片显示功能演示. * * - 演示如何将比较大的图片通过多种方法显示到屏幕上. * - 运行的会很缓慢, 因为示例的图片文件比较大. * 1. 在运行过ex01或者ex02的开发板上 编译烧录本程序. * 2. 将该项目data文件夹内的所有文件放置于SD卡的根目录上. * 就是SD卡内放data文件夹内的所有文件, 不能额外套文件夹. * 如果你的SD卡插入电脑上显示为可移动磁盘, 那么双击SD卡目录就要看到这个文件夹里的文件 * * 将根目录下的data文件夹 上传到LittleFS之后运行效果更佳 * 或者可以准备一张SD卡,并准备在卡的根目录下放置data文件夹内的文件. * 就是SD卡内放data文件夹内的所有文件, 不能额外套文件夹. * 如果你的SD卡插入电脑上显示为可移动磁盘, 那么双击SD卡目录就要看到这个文件夹里的文件 * * 默认的文件系统为SD卡. 当没有插入SD卡时, 会读取LittleFS文件系统. * 没有条件准备SD卡的, 可以烧录LittleFS文件系统. * * 对于ESP8266 Arduino 用户, 在项目草图文件夹内新建一个data文件夹, 并放入文件 (示例已提供data文件夹) * 再在 arduinoIDE 的工具选项里选择 ESP8266 LittleFS Data Upload. * 没有这个选项的需要参考以下文档安装ESP8266 Sketch upload tool * https://randomnerdtutorials.com/install-esp8266-nodemcu-littlefs-arduino/ * * 对于ESP32 Arduino 用户, 也要在项目草图文件夹内放一个data文件夹, 并把文件放入其中 (示例已提供data文件夹) * 再在 arduinoIDE 的工具选项里选择 ESP32 Sketch data upload, 最后选择LittleFS. * 没有这个选项的需要参考以下文档安装ESP32 LittleFS upload tool * https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/ * * 对于PlatformIO 用户, 需要进入platformIO扩展界面, 选择Upload Filesystem Image, 上传项目文件. * ESP8266和ESP32都要用这种方法. * * {0} 代码食用注意事项: * 这一部分的代码很难读, 或者按维莫斯小姐的说法, 很 "抽象" . * 如果你要开始一行一行读取下去, 不妨先在这里停顿一下, 看一下一些需要你提前知道的东西. * * {1} 画布, 或者叫sprite. 是一块专门存储图像的数据结构. * 画布需要大量的内存来存放图像的像素, 但是你可以对画布进行任何图像操作, 就像是在墨水屏上能进行的操作一样多. * 画布类型的数据结构至少需要包含3个要素: 宽度, 高度, 每像素需要的字节数. * 画布需要的内存为 宽度*高度*每像素需要的字节数. (1bit图像需要8分之一字节) * 在画布上可以进行几乎任何图像操作, 就像是在墨水屏上能进行的操作一样多. * 而且你还可以获取像素的颜色 (这和你创建的画布的颜色格式有关). * 画布可以随时被快速的显示到墨水屏上, 显示完了之后再使用display方法, 你刚才创建的画布就显示出来了. * * {2} 屏幕缓存. 你就从来没好奇过, 墨水屏上怎么呈现图像的? 我好奇过. * 我翻看了里面那些错综复杂又是深奥的C++骚操作又是类继承又是虚函数指针...反正就是抽象而且难懂的的源代码, * 简而言之, 墨水屏显示的原理就是墨水屏驱动里面也有个画布, 不过这个画布是1bit的(每8像素消耗1字节内存). * 所有对墨水屏进行的操作其实都是对那个内部的画布进行的操作. * 在你使用墨水屏的display方法时, 呃, 墨水屏的驱动程序可以读取到这个屏幕缓存的所有字节. * 并且根据驱动手册的方法将这个画布上的字节发送到屏幕上. * 发送完成之后再通过一些代码, 墨水屏就会刷屏, 将这个屏幕缓存里的内容显示出来了. * 维莫斯小姐: 苏滴嘶内~ * 下面的库其实用了一些更加骚气的方法, 直接读取了这块画布, 从而能允许你在ESP8266上也可以...显示不小的图片 * * {3} 灰度显示. ReadGuy这个库的一大特色就是支持灰度显示. * 欸, 可是屏幕缓存画布只有1bit啊, 怎么显示灰度啊? * 不用急, 原理也不难. * 16级灰度颜色的刷屏分为15个步骤. 因为白色不需要刷. * 在开始刷屏之前, 务必进行一次慢刷, 确保所有的白色像素都足够白. * 第一次刷屏, 通常会刷最浅的灰色. * 此时, 首先将屏幕缓存内所有需要显示为这个颜色的像素全写入为黑色(只是名义上的黑色, 实际上是设定的颜色). * 再初始化墨水屏的刷屏功能. * - 此处需要额外设定刷屏的颜色的参数. 此参数设定是可以由驱动程序自动完成的, 不过也可以通过你调用函数完成. * - 用户只需要用简单的setDepth函数设置颜色即可. 用户设定的颜色深度通常用于显示字符串或者形状等GUI控件. * - 刷图程序的灰度图片刷新功能会自动控制刷屏颜色, 不需要手动调用. * 最后刷屏. 因为之前刷图程序已经设定过刷的颜色了, 所以此时刷屏呈现的颜色就是之前设定的颜色. * 第2-15次刷屏原理类似. * 每次追加黑色像素, 追加的像素就是来显示灰度的像素. 而原本的黑色像素就是之前那些灰度颜色的像素. * * \*(^_^)*/ /** 我相信你应该还不懂. 我就举个例子吧. * 比如要显示一个渐变色, 从左到右为白色→黑色的渐变图. * 首先进行一次慢刷, 确保要刷图的地方都是纯洁的白色. * ≡=-(1)-=≡ * 筛选出最浅的灰色设为灰1, (灰X代表灰度颜色, 灰15代表黑色) * 再把图片内所有颜色和灰1最接近的颜色写入到屏幕缓存内. * 此时屏幕缓存内的"黑色"部分都是与灰1最接近的颜色, 其他颜色均为"白色"部分. * 刷好了之后, 进行一次刷屏. (系统自动调用setDepth(1)来保证接下来图片的黑色部分其实是灰1). * ≡=-(2)-=≡ * 筛选出比最浅的灰色深一点的颜色设为灰2, 把图片内和灰2最接近的颜色写入屏幕缓存, 但保持原本代表灰1的"黑色"不变. * (也就是说, 把灰2的像素写入之后, 屏幕缓存的黑色像素只多不少. 写入过程中, 只能是白→黑, 不存在黑→白) * 此时屏幕缓存内的"黑色"部分包含了与灰2最接近的颜色, 和刚才刷完的与灰1最接近的颜色. 其他颜色均为"白色"部分. * 刷好了之后, 进行一次刷屏. (系统自动调用setDepth(2)来保证接下来图片的黑色部分其实是灰2) * 注意屏幕显示, 原本的灰1并没有颜色变化, 变化的都是本次追加的"灰2"代表的"黑色". * 追加的像素会显示为新设定的颜色. * ≡=-(3)-=≡ * 对于灰3, 和灰2一样, 追加显示到屏幕缓存内. * (也就是说, 屏幕缓存的黑色像素还是只多不少. 写入过程中, 只能是白→黑, 不存在黑→白) * 此时屏幕缓存内的"黑色"部分包含了与灰3, 灰2和灰1最接近的颜色. 其他颜色均为"白色"部分. * 设置颜色深度, 刷屏. * 对于灰4~灰15, 都这么做 * ≡=-(5)-=≡ * 刷完灰15之后, 灰度图就刷完了. * * {4} 自定义刷屏函数. 到底是什么决定了刷屏时的每一像素是什么颜色? 是黑是白? * 其实刷屏的本质就是通过调用一个外部函数. * 该函数可以根据输入的像素坐标位置来决定输出的像素颜色. * 为了简化程序调用过程并提高调用速度, 此处的像素坐标位置参数为一个整数(而不是两个) * 至于该怎么调用这个函数, 并不是你需要了解的事情. * * @note 重要消息: 这是一个实验性功能. 可能你所使用的LGFX库版本较旧而无法通过编译. * 如果你的项目中无法成功编译源码中的setBuffer, 请更改LovyanGFX库的函数! * 位于文件 LovyanGFX/src/lgfx/v1/LGFX_Sprite.hpp * 第155行 void setBuffer 函数: * 修改为如下内容并保存 * ``` C++ void setBuffer(void* buffer, int32_t w, int32_t h, color_depth_t bpp = rgb565_2Byte) { deleteSprite(); if (bpp != 0) { _write_conv.setColorDepth(bpp); _read_conv = _write_conv; _panel_sprite.setColorDepth(bpp); } _panel_sprite.setBuffer(buffer, w, h, &_write_conv); _img = _panel_sprite.getBuffer(); _sw = w; _clip_r = w - 1; _xpivot = w >> 1; _sh = h; _clip_b = h - 1; _ypivot = h >> 1; } ``` * 完成后请再次尝试编译 * [已经向lovyan03/LovyanGFX发布issue, 等待解决] * * @attention * Copyright (c) 2022-2023 FriendshipEnder * * Apache License, Version 2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ //在这里包含程序需要用到的库函数 #include //arduino功能基础库. 在platformIO平台上此语句不可或缺 #include "readguy.h" //包含readguy_driver 基础驱动库 #include "guy_image.h" //包含 readguy_image 图片功能库. 此库文件放在项目文件夹内. constexpr const char * BMP_FILE = "/bmp.bmp"; //在此更改你的图片文件路径名 constexpr const char * JPG_FILE = "/jpg.jpg"; //在此更改你的图片文件路径名 constexpr const char * PNG_FILE = "/png.png"; //在此更改你的图片文件路径名 ReadguyDriver guy;//新建一个readguy对象, 用于显示驱动. void setup(){ // 1 - 初始化和启动ReadGuy --------------------- 1 - 初始化和启动ReadGuy -------< Serial.begin(115200); //初始化串口 guy.init(); //初始化readguy_driver 基础驱动库. guy.setRotation(3); //设置旋转方向. 因为此库的功能不随旋转而发生变化. // 2 - 显示图片 方式1 --------------------- 2 - 显示图片 方式1:使用readguy提供的drawImage方法. ------<< //此功能可以在 画布{1}(Sprite) 绘制图片, 再使用内置的drawImage方法来显示出这个sprite. //此方法消耗的内存颇多, 需要为整个sprite分配所有内存才能成功绘制. //由于目前版本LovyanGFX的限制, 不能分配4bit的画布, 只能分配大小至少为8bit的画布. guy.fillScreen(1); //清屏 //guy.display(); //显示白屏,用于将来显示图片. LGFX_Sprite sp(&guy); //创建一个 画布(Sprite) (可以存储一些像素, 快速读写) sp.setColorDepth(lgfx::v1::color_depth_t::grayscale_8bit); //设置画布sprite的色彩深度 sp.createSprite(80,80); //为刚刚创建的画布对象分配内存 (像素宽度*像素高度). 此处为80(宽)*80(高)像素 sp.drawBmpFile(guy.guyFS(),BMP_FILE,0,0); //从文件创建BMP图像信息. 文件系统内必须要有这个BMP图像. //默认的文件系统为SD卡. 当没有插入SD卡时, 会读取LittleFS文件系统. //没有条件准备文件系统的, 可以烧录文件系统. Serial.printf("[%lu] sp.w: %d, h: %d, res: %im.\n",millis(),sp.width(),sp.height(),ESP.getFreeHeap()); guy.drawImage(sp,9,11); //使用抖动像素的方式显示图片(不是灰度, 只有黑点和白点的那种显示效果) //sp为要显示的画布, 后面的9 11都是要显示的目标坐标位置. 此处代表将画布显示在 屏幕缓存{2} 的第9列, 第11行. Serial.printf("[%lu] drawn dithering bmp.\n",millis()); //显示信息 guy.display(); //1.2.0版本之后, drawImage不再刷屏, 需要额外调用display函数刷屏. delay(2000); guy.display(FILL_WHITE,READGUY_FAST); //在保持屏幕缓存不变的时候快速刷新白屏. guy.drawImage(sp,0,0,guy.width(),guy.height()); //绘画的画布可以被放大或者缩小到任意宽度和高度. //此处的参数调用表示将会在屏幕坐标(0,0)开始显示, 显示的画布宽度缩放到屏幕宽度, 画布高度缩放到屏幕高度. guy.display(); //调用display函数刷屏. delay(2000); guy.display(FILL_WHITE,READGUY_FAST); //在保持屏幕缓存不变的时候快速刷新白屏 guy.fillScreen(1); //白屏清屏(清屏幕缓存) guy.drawImage(sp,10,10,65,50); //缩放: 缩小到65X50 guy.display(); //调用display函数刷屏. delay(2000); //以下函数可以允许你用灰度显示画布. //在刷灰度之前, 该函数可以对刷屏区域自动慢刷白屏. //0-关闭连续刷屏 开启16阶灰度抖动. 连续刷屏并非所有的屏幕都支持. 所以此处使用了一个函数检验. if(guy.supportGreyscaling()==-16){ //此函数返回-16代表支持连续刷屏. 返回16代表支持灰度, 返回0代表仅黑白. guy.fillScreen(1); //白屏清屏(清屏幕缓存) guy.setGreyQuality(0); //设置刷屏方式:0-关闭连续刷屏, 开启16阶灰度抖动. guy.draw16grey(sp,9,9); //使用16级灰度的方式显示图片 需要的时间比较长 delay(2000); } //1-开启连续刷屏 开启16阶灰度抖动 (默认值) guy.fillScreen(1); //白屏清屏(清屏幕缓存) guy.setGreyQuality(1); //设置刷屏方式:1-开启连续刷屏, 开启16阶灰度抖动. 在初始化时, 这是默认值. guy.draw16grey(sp,12,12,120,120); //使用16级灰度的方式显示图片. 后两个参数120代表缩放之后的宽度高度 delay(2000); //2-关闭连续刷屏 关闭16阶灰度抖动. 连续刷屏并非所有的屏幕都支持. 所以此处使用了一个函数检验. if(guy.supportGreyscaling()==-16){ //此函数返回-16代表支持连续刷屏. 返回16代表支持灰度, 返回0代表仅黑白. guy.fillScreen(1); //白屏清屏(清屏幕缓存) guy.setGreyQuality(2); //2-关闭连续刷屏, 关闭16阶灰度抖动 guy.draw16grey(sp,9,9,50,100); //使用16级灰度的方式显示图片. 参数50和100分别代表宽度缩放和高度缩放 delay(2000); } //3-开启连续刷屏 关闭16阶灰度抖动 guy.fillScreen(1); //白屏清屏(清屏幕缓存) guy.setGreyQuality(3); //3-打开连续刷屏, 关闭16阶灰度抖动 guy.draw16grey(sp,3,3,100,50); //使用16级灰度的方式显示图片 参数100和50分别代表宽度缩放和高度缩放 delay(2000); //灰度显示画布到此显示完了 sp.deleteSprite(); //关闭画布, 释放图片占用的大量内存 Serial.printf("[%lu] drawn 16-layer greyscale bmp.\n",millis()); //显示信息 delay(2000); // 3 - 显示图片 方式2 --------------------- 3 - 显示图片 方式2:使用readguyImage类. -----<<< //readguy驱动程序的内部有一个屏幕缓存, 可以用于存储显示引擎显示的内容, 在刷屏时 直接发送此缓存的内容. //显示引擎会通过显示函数, 来改写屏幕缓存的内容. (显示就是这样显示的. 类似于print, drawPixel, fillRect...) //此绘制方式可以做到几乎不消耗额外的内存, 仅需要屏幕缓存即可显示图片. //适合用在esp8266或者消耗的内存较多的项目上. //显示原理比较复杂, 因为显示函数的限制, 图片不能直接显示到屏幕缓存中. //大体上说就是用屏幕缓存作为显示图片的中间内存, 再分阶段解码图片, 最后通过自定义刷屏函数, 完成刷新图片 //使用此方法, 因为屏幕缓存被复用作图片解码中间内存, 屏幕缓存上原本的内容会因为图片解码而发生改变. //不需要指定额外的外部内存. //显示BMP格式 readguyImage im(guy); //定义一个绘制器, 此类中的函数用于绘制图片. //所有的绘制图片的参数均需要放入此结构内 //直接更改im内的数据即可设置绘制参数. (就像结构体一样用它) im.baseFs=&guy.guyFS(); //在此处就是设置文件系统. im.filename=BMP_FILE; //在此直接设置文件路径和文件名. guy.display(FILL_WHITE,READGUY_SLOW); //将屏幕全刷成白屏. 为了即将的图片刷新. //建议在使用drawImageFile函数之前, 使用慢刷刷白屏, 可以保证显示效果清晰可见. im.drawImageFile(); //显示BMP格式.图片. im会自动识别文件扩展名并绘制. //此处不再需要调用 guy.display() 函数即可显示. //此时显示缓存已经清空(因为显示缓存内原本是用来解码图片用的) //此时直接使用guy.display()将刷白屏. delay(2000); //显示JPG格式 im.filename=JPG_FILE; //在此直接设置文件路径和文件名. im.scalex=0.3333333f; //设置横坐标图片缩放 im.scaley=0.3333333f; //设置纵坐标图片缩放. im.offsetx=70; //设置显示偏移. 此处的70说明 JPG图像应该从图片文件的(70,40)坐标处开始解码 im.offsety=40; //设置显示偏移Y轴方向. im.background=0; //设置背景颜色, 0黑1白, 此处设为背景色为黑色. guy.display(FILL_WHITE,READGUY_SLOW); //将屏幕全刷成白屏. 为了即将的图片刷新. im.drawImageFile(); //显示JPG格式.图片. im会自动识别文件扩展名并绘制. delay(2000); #ifndef ESP8266 //显示PNG格式. ESP8266不支持PNG格式. im.filename=PNG_FILE; //PNG图片. ESP8266的内存不够, 所以不能显示PNG格式~(T_T)~ im.x=10; //设置在哪里绘制图片. 在此编辑图片绘制起始点的 X, Y 坐标 im.y=5 ; // im.scalex=400.0f/1280.0f; im.scaley=300.0f/576.0f; guy.display(FILL_WHITE, READGUY_SLOW);//显示. 此处的功能就是将显示缓存输出到屏幕上 im.drawImageFile(); //显示PNG格式.图片. ESP8266可能不会绘制. delay(2000); #endif //开启灰度显示, 然后显示BMP格式 im.filename=BMP_FILE; //在此直接设置文件路径和文件名. im.scalex=0.75f; //设置横坐标图片缩放 im.scaley=0.5625f; //设置纵坐标图片缩放. im.offsetx=0; //设置显示偏移. 此处的70说明 JPG图像应该从图片文件的(70,40)坐标处开始解码 im.offsety=0; //设置显示偏移Y轴方向. im.background=1; //设置背景颜色, 0黑1白, 此处设为背景色为黑色. //预分配内存, 可以用作显示缓存. #ifdef ESP8266 #define MEM_POOL 10000 #else #define MEM_POOL 60000 #endif uint8_t *buf = new uint8_t [MEM_POOL]; //分配缓存 im.exPool=buf; //设置外部缓存内存地址 im.exPoolSize=MEM_POOL; //设置外部缓存内存大小 guy.setGreyQuality(1); //设置灰度模式为默认灰度显示模式 guy.display(FILL_WHITE,READGUY_SLOW); //将屏幕全刷成白屏. 为了即将显示灰度图. im.enableFloyd=0; //禁用掉抖动算法. im.drawImageFile(1); //显示JPG格式.图片. delay(2000); guy.display(FILL_WHITE,READGUY_SLOW); //将屏幕全刷成白屏. 为了即将显示灰度图. im.enableFloyd=1; // 重新启用抖动算法. im.drawImageFile(1); //显示JPG格式.图片. delay(5000); delete [] buf; //缓存用完了, 释放内存. im.exPool=nullptr; //用完之后将这个内存地址置为null, 避免重复使用导致内存不可访问. im.exPoolSize=0; // --------------------- 4 - 显示图片 使用readguyImage类的externalBuffer绘制到主屏幕缓存. -----<<< //实现原理: 通过一个额外的缓存, 显示引擎可以分多次将图片显示在这个额外的内存中, 再将这块内存写入到缓存内. //由于用了外部内存, 所以可以分多次显示一张大图片, 相比于分配整张大图片的内存, 要更节省内存.代价是速度更慢 //readguy驱动程序的内部有一个屏幕缓存, 如果使用这个缓存来显示图片, 屏幕上原本的东西也会变得混乱.(上个示例) //因此, readguyImage类提供了使用外部内存的显示功能. //分为多次, 每次将图片的一小部分显示到外部内存中, 再写入到屏幕缓存内. //需要提供额外分配好的内存, 但此函数不会更改显示区域之外的显示缓存. guy.fillScreen(1); //将屏幕全刷成白屏. 为了即将的图片刷新. for(int i=1;i<10;i++){ //在屏幕上绘画线段. 本质是更改显示缓存. guy.drawLine(i*guy.width()/10,0,0,i*guy.height()/10,0); guy.drawLine(i*guy.width()/10,guy.height()-1,guy.width()-1,i*guy.height()/10,0); } guy.drawLine(guy.width(),0,0,guy.height(),0); guy.display(READGUY_SLOW); //刷新屏幕, 显示绘画的线段 //im.baseFs=&guy.guyFS(); //直接更改im内的数据即可设置绘制参数. 在此处就是设置文件系统. im.filename=BMP_FILE; //在此直接设置文件路径和文件名. im.background=0; //设置背景颜色, 0黑1白, 此处设为背景色为黑色. im.x=15; //设置将要显示的横坐标 im.y=15; //设置将要显示的纵坐标 im.w=180; //设置将要显示区域的宽度 im.h=270; //设置将要显示区域的高度 im.offsetx=0; //如果不从左上角开始解码图片, 从哪个坐标开始显示/解码图片? im.offsety=0; im.scalex=0.5625f; //设置横坐标缩放. im.scaley=0.5625f; //设置纵坐标缩放. 为0代表与横坐标缩放相同. buf = new uint8_t [ 60*270 ]; //分配缓存 im.exPool=buf; //设置外部缓存内存地址 im.exPoolSize=60*270; //设置外部缓存内存大小 im.drawImageToBuffer(); //将图片写入到屏幕缓存. im会自动识别文件扩展名并绘制. guy.display(); //显示缓存.此时应当能看到图片. delete [] buf; //缓存用完了, 释放内存. im.exPool=nullptr; //用完之后将这个内存地址置为null, 避免重复使用导致内存不可访问. im.exPoolSize=0; } void loop(){ delay(1); }/* END OF FILE. ReadGuy project. Copyright (C) 2023 FriendshipEnder. */