まず…なぜGIFなのか?
「esp32とTFT液晶で強震モニタを作りたい」
を前提に色々試行錯誤しています。前のTTSの記事もその一環です。強震モニタとはリアルタイムで緊急地震速報(予報も含める)を見ることができるアレです。
webページを分析してみると
リアルタイムにGIFファイルをDLしてを重ねて表示しているようです。なのでGIFファイルを表示する必要があるのです。
ESP32でTFT液晶(ILI9486)を使う準備
ILI9486である必要はありませんが、TFT液晶を使うとなるとライブラリが必須になります。
TFT液晶用のライブラリは何個か候補があります。僕が渡り歩いたのは下の3つです。
Adafruit-GFX-Library
TFT_eSPI
LovyanGFX
この中で一番高機能、しかも日本人なら、LovyanGFX一択です!!開発者の「らびやん」さんは日本人でリファレンスも多くあり高性能なライブラリです。書き出し時に拡大・縮小ができたり、日本語出力が標準でできたり(これは便利!!)、海外ライブラリにはない便利なことができて大変助かりました。Adafruitの謹製ライブラリは速度面で遅いイメージ、TFT_eSPIも高機能ですが、日本語出力なんて海外ライブラリにあるわけもなく…リファレンスを読むと
最初からLovyanGFX使っとけばよかったOTL
基本的な命令は標準ライブラリやTFT_eSPTと同じメソッドですのですんなり移行できます。
esp32との接続やデモの表示の例はたくさんありますので、そちらを参考に画面表示できるまでしときます。これができないとデコードしても表示までたどり着けません。
GIFのデコーダーを探しまくる
LovyanGFXではJPEGはlcd.drawJpgFileですぐ表示できるようです。TJpg_Decoderとかでデコードしてたのは何だったんだろうと思ってしまいます。
GIFもデコーダあるのかなぁ(ワクワク
チーン。AnimatedGIFを使いましょうということですね。
らびやんさんが言うなら間違いないです!AnimatedGIFのリファレンスを読んでみました。結構、機能満載なライブラリでして、
もっとGIFファイル(静止画)をサッとデコードできそうな簡単なライブラリはないかなぁ~(贅沢
と探してはみたんですけど意外にないんですねwで、見つけた記事が
です。この記事を書いた人も軽量デコーダーを探していました。なんと下の方にシンプルなデコーダのソースコードが載っているではないですか。
GIFデコーダの改良
さきほどの記事のデコーダを早速コピペして動かしたところ、GIFファイルが表示できません。というか、いろいろ試すと表示できるのもあればできないものもあります。よく調べてみるとinit_decoder()で負数が返ってきているようです。
これは…GIFファイルのヘッダをうまく読めてないようです。ヘッダを正しく読んでいないので透過GIFにも対応していないということになります。
さぁ~次いこー(棒
としましたが、あまりに探して見つからないので重い腰を上げることにしました。今考えると意地になってましたねw
GIFのヘッダの仕様書を読みながらライブラリを改良してみました。どうやらExtensionを正しく読んでいない様子。正しく読むようにして透過GIFへの対応をしました。長いですが下にコードを張ります
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
#ifndef __GIF_MODI_H__ #define __GIF_MODI_H__ //typedef void(*gif_draw_pixel_t)(unsigned char r, unsigned char g, unsigned char b); typedef void(*gif_draw_pixel_t)(unsigned char r, unsigned char g, unsigned char b , bool trans_gif , bool trans_pixel); typedef struct{ unsigned short SCREEN_WIDTH; unsigned short SCREEN_HEIGHT; unsigned char BYTE_5; unsigned char BACKGROUND; unsigned char ASPECT_RATIO; } SCREEN_DESC; typedef struct{ unsigned short LEFT; unsigned short UP; unsigned short IMAGE_WIDTH; unsigned short IMAGE_HEIGHT; unsigned char BYTE_9; } IMAGE_DESC; typedef struct{ unsigned char BLOCK_SIZE; //Block Size(1 Byte) 0x04固定 unsigned char BYTE_2; //Reserved(3 Bits),Disposal Mothod(3 Bits),User Input Flag(1 Bit),Transparent Color Flag(1 Bit) bool TC_FLAG; //Transparent Color Flag(1 Bit) unsigned short DELAY_TIME; //Delay Time(2 Bytes) unsigned char TC_INDEX; //Transparent Color Index(1 Byte) } GC_EXTENTION; typedef struct{ unsigned char R; unsigned char G; unsigned char B; } COLOR; typedef struct{ SCREEN_DESC screenDesc; IMAGE_DESC imageDesc; GC_EXTENTION gcExtention; } GIF_IMAGE; typedef struct{ unsigned short code; unsigned short prevCode; unsigned short nextCode; } DICTIONARY_ENTRY; class GIF { private: //unsigned char BUFFER[16]; unsigned char BUFFER[256]; unsigned short PrimaryCodeSize, PrimaryDictSize; unsigned short currentCodeSize, currentDictSize; unsigned short code, oldcode = 0; unsigned short code1, code2; unsigned short code_CLEAR, code_END; COLOR g_GlobalColorTable[256]; #define MAX_DICT_SIZE 4098 DICTIONARY_ENTRY Dictionary[MAX_DICT_SIZE]; unsigned char bitsRemaining; unsigned short getNextCode(void); int checkSignature(char*); File g_fileObject; gif_draw_pixel_t gif_draw_pixel; int read_file(File, unsigned char *, int); unsigned char read_code; unsigned char byte5; unsigned char byte9; unsigned char byte2; // Graphic Control Extension用追加 bool trans_gif; //gif_draw_pixelへの引き渡し用 unsigned char trans_color_index; unsigned char currentDataSectionLength; public: int init_decoder(File, GIF_IMAGE *); int drawGIFImage(gif_draw_pixel_t); unsigned char counter; void GIFDrawPixel(unsigned char); unsigned short imgWidth, imgHeight; }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
#include <FS.h> #include "gif_modi.h" int GIF::init_decoder(File fileObject, GIF_IMAGE * gifImage) { unsigned int i; unsigned char bpp; unsigned short minLZWCodeLength; unsigned int GCT_size; g_fileObject = fileObject; for(int i=0 ; i < 16 ; i++) BUFFER[i] = 0; //バッファをリセットしないと再利用できない //GIF Headerの読み込み //Signature(3 Bytes) + Version(3 Bytes) "GIF89a" or "GIF87a" read_file(g_fileObject, BUFFER, 6); if (checkSignature((char*)BUFFER) != 1) return -1; read_file(g_fileObject, BUFFER, 7); //Logical Screen Width(2 Bytes) (*gifImage).screenDesc.SCREEN_WIDTH = (BUFFER[1] << 8) + BUFFER[0]; //Logical Screen Height(2 Bytes) (*gifImage).screenDesc.SCREEN_HEIGHT = (BUFFER[3] << 8) + BUFFER[2]; //Global Color Table Flag(1 Bit),Color Resolution(3 Bits),Sort Flag(1 Bit),Size of Global Color Table(3 Bits) (*gifImage).screenDesc.BYTE_5 = BUFFER[4]; //Background Color Index(1 Byte) (*gifImage).screenDesc.BACKGROUND = BUFFER[5]; //Pixel Aspect Ratio(1 Byte) (*gifImage).screenDesc.ASPECT_RATIO = BUFFER[6]; byte5 = (*gifImage).screenDesc.BYTE_5; //Size of Global Color Table(3 Bits) bpp = (byte5 & 0x07); //2の(Size of Global Color Table)乗個のGlobal Color Tableがある GCT_size = 2 << bpp; //Global Color Table Flag(1 Bit)があるなら if (byte5 & 0x80){ for (i = 0; i < GCT_size; i++){ //1つの色情報につきRGBの3バイトずつ、GCT_size個読む read_file(g_fileObject, BUFFER, 3); g_GlobalColorTable[i].R = BUFFER[0]; g_GlobalColorTable[i].G = BUFFER[1]; g_GlobalColorTable[i].B = BUFFER[2]; } } else return -1; //Image Separator(1B) = 0x2cが出るまでエクステンションを正しく読み飛ばす do { read_file(g_fileObject, BUFFER, 1); //Extensionコードがあるなら if (BUFFER[0] == 0x21){ read_file(g_fileObject, BUFFER, 1); //Block: Graphic Control Extension if (BUFFER[0] == 0xf9){ read_file(g_fileObject, BUFFER, 6); //Block Size(1 Byte) 0x04 固定値 (*gifImage).gcExtention.BLOCK_SIZE = BUFFER[0]; //Reserved(3 Bits),Disposal Mothod(3 Bits),User Input Flag(1 Bit),Transparent Color Flag(1 Bit) (*gifImage).gcExtention.BYTE_2 = BUFFER[1]; byte2 = (*gifImage).gcExtention.BYTE_2; if(byte2 & 0x01){ (*gifImage).gcExtention.TC_FLAG = true; }else{ (*gifImage).gcExtention.TC_FLAG = false; } trans_gif = (*gifImage).gcExtention.TC_FLAG; //Delay Time(2 Bytes) (*gifImage).gcExtention.DELAY_TIME = (BUFFER[3] << 8) + BUFFER[2]; //Transparent Color Index(1 Byte) (*gifImage).gcExtention.TC_INDEX = BUFFER[4]; trans_color_index = (*gifImage).gcExtention.TC_INDEX; } //Block: Comment Extension if (BUFFER[0] == 0xfe){ read_file(g_fileObject, BUFFER, 1); //unsigned char c_size = BUFFER[0]; //read_file(g_fileObject, BUFFER, c_size); do{ read_file(g_fileObject, BUFFER, 1); } while (BUFFER[0] != 0x00); continue; } //Block: Plain Text Extension if (BUFFER[0] == 0x01){ read_file(g_fileObject, BUFFER, 13); read_file(g_fileObject, BUFFER, 1); //unsigned char t_size = BUFFER[0]; //read_file(g_fileObject, BUFFER, t_size); do{ read_file(g_fileObject, BUFFER, 1); } while (BUFFER[0] != 0x00); continue; } //Block: Application Extension if (BUFFER[0] == 0xff){ read_file(g_fileObject, BUFFER, 12); read_file(g_fileObject, BUFFER, 1); //unsigned char a_size = BUFFER[0]; //read_file(g_fileObject, BUFFER, a_size); do{ read_file(g_fileObject, BUFFER, 1); } while (BUFFER[0] != 0x00); continue; } } } while (BUFFER[0] != 0x2C); //Block: Image Blockの読み込み read_file(g_fileObject, BUFFER, 11); //Image Left Position(2 Bytes) (*gifImage).imageDesc.LEFT = (BUFFER[1] << 8) + BUFFER[0]; //Image Top Position(2 Bytes) (*gifImage).imageDesc.UP = (BUFFER[3] << 8) + BUFFER[2]; //Image Width(2 Bytes) (*gifImage).imageDesc.IMAGE_WIDTH = (BUFFER[5] << 8) + BUFFER[4]; //Image Height(2 Bytes) (*gifImage).imageDesc.IMAGE_HEIGHT = (BUFFER[7] << 8) + BUFFER[6]; //Local Color Table Flag(1 Bit),Interlace Flag(1 Bit),Sort Flag(1 Bit),Reserved[未使用](2 Bits),Size of Local Color Table(3 Bits) (*gifImage).imageDesc.BYTE_9 = BUFFER[8]; imgWidth = (*gifImage).imageDesc.IMAGE_WIDTH; imgHeight = (*gifImage).imageDesc.IMAGE_HEIGHT; byte9 = (*gifImage).imageDesc.BYTE_9; //LZW Minimum Code Side(1 Byte) minLZWCodeLength = BUFFER[9] + 1; //Block Size(1 Byte) currentDataSectionLength = BUFFER[10]; code_CLEAR = GCT_size; code_END = GCT_size + 1; PrimaryDictSize = GCT_size + 2; PrimaryCodeSize = minLZWCodeLength; currentCodeSize = minLZWCodeLength; return 0; } int GIF::read_file(File fileObject, unsigned char *buf, int count){ return fileObject.read(buf,count); } int GIF::drawGIFImage(gif_draw_pixel_t draw_pixel_func){ unsigned int i; gif_draw_pixel = draw_pixel_func; currentDictSize = PrimaryDictSize; counter = 0; for (i = 0; i < MAX_DICT_SIZE; i++) Dictionary[i].prevCode = Dictionary[i].nextCode = 0; bitsRemaining = 0; while ((code = getNextCode()) != code_END){ if (code == code_CLEAR){ currentCodeSize = PrimaryCodeSize; currentDictSize = PrimaryDictSize; oldcode = getNextCode(); if (oldcode > currentDictSize){ return -3; } GIFDrawPixel(oldcode); continue; } if (code < currentDictSize){ code1 = code; code2 = 0; while (code1 >= PrimaryDictSize){ Dictionary[code1 - PrimaryDictSize].nextCode = code2; code2 = code1; code1 = Dictionary[code1 - PrimaryDictSize].prevCode; if (code1 >= code2) return -3; } GIFDrawPixel(code1); while (code2 != 0){ GIFDrawPixel(Dictionary[code2 - PrimaryDictSize].code); code2 = Dictionary[code2 - PrimaryDictSize].nextCode; } Dictionary[currentDictSize - PrimaryDictSize].code = code1; Dictionary[currentDictSize - PrimaryDictSize].prevCode = oldcode; ++currentDictSize; if (currentDictSize == MAX_DICT_SIZE) return -2; if ((currentDictSize) == (0x0001 << currentCodeSize)) ++currentCodeSize; if (currentCodeSize > 12) currentCodeSize = 12; oldcode = code; } else { code1 = oldcode; code2 = 0; while (code1 >= PrimaryDictSize){ Dictionary[code1 - PrimaryDictSize].nextCode = code2; code2 = code1; code1 = Dictionary[code1 - PrimaryDictSize].prevCode; if (code1 >= code2) return -3; } GIFDrawPixel(code1); while (code2 != 0){ GIFDrawPixel(Dictionary[code2 - PrimaryDictSize].code); code2 = Dictionary[code2 - PrimaryDictSize].nextCode; } GIFDrawPixel(code1); Dictionary[currentDictSize - PrimaryDictSize].code = code1; Dictionary[currentDictSize - PrimaryDictSize].prevCode = oldcode; ++currentDictSize; //std::cout << "dictionary size: " << std::dec << currentDictSize << std::endl; if (currentDictSize == MAX_DICT_SIZE) return -2; if ((currentDictSize) == (0x0001 << currentCodeSize)) ++currentCodeSize; if (currentCodeSize > 12) currentCodeSize = 12; oldcode = code; } } return 0; } int GIF::checkSignature(char * IN){ char signature[7]; int i; for (i = 0; i <= 6; i++) signature[i] = IN[i]; if ((strcmp(signature, "GIF87a") == 0) || (strcmp(signature, "GIF89a") == 0)) return 1; else return 0; } unsigned short GIF::getNextCode(void){ unsigned int retval = 0, temp; if (bitsRemaining >= currentCodeSize){ retval = (read_code & ((0x01 << currentCodeSize) - 1)); read_code >>= currentCodeSize; bitsRemaining -= currentCodeSize; } else { retval = (read_code & ((0x01 << bitsRemaining) - 1)); read_file(g_fileObject, &read_code, 1); ++counter; if (counter == currentDataSectionLength){ counter = 0; read_file(g_fileObject, ¤tDataSectionLength, 1); } if ((currentCodeSize - bitsRemaining) <= 8){ temp = (read_code & ((0x01 << (currentCodeSize - bitsRemaining)) - 1)); retval += (temp << bitsRemaining); read_code >>= (currentCodeSize - bitsRemaining); bitsRemaining = 8 - (currentCodeSize - bitsRemaining); } else { retval += (read_code << bitsRemaining); read_file(g_fileObject, &read_code, 1); ++counter; if (counter == currentDataSectionLength){ counter = 0; read_file(g_fileObject, ¤tDataSectionLength, 1); } retval += ((read_code & ((0x01 << (currentCodeSize - bitsRemaining - 8)) - 1)) << (bitsRemaining + 8)); read_code >>= (currentCodeSize - bitsRemaining - 8); bitsRemaining = 8 - (currentCodeSize - bitsRemaining - 8); } } return retval; } void GIF::GIFDrawPixel(unsigned char code){ COLOR rgbColor; bool trans_pixel = false; rgbColor = g_GlobalColorTable[code]; if(trans_gif && code == trans_color_index) trans_pixel = true; (*gif_draw_pixel)(rgbColor.R, rgbColor.G, rgbColor.B, trans_gif , trans_pixel); } |
基本的に使い方はこのライブラリが紹介してあったサイトのサンプルコードで動きますが、コールバック関数が
(*gif_draw_pixel)(rgbColor.R, rgbColor.G, rgbColor.B, trans_gif , trans_pixel);
で返ってきますので修正してください。デコードしたピクセル毎に呼び出されます。trans_gifは透過GIFファイルかどうか、tens_pixelは透過ピクセルかどうかという変数を加えていますので透過処理も可能となります。これで正しくデコードはできるようになりました。
余談ですが、汎用性・互換性の為、このライブラリもイニシャライズ時にファイルストリームを渡して初期化していますが、強震モニタを作る上で画像生成の流れ的に
- GIFダウンロード
- GIFデコード
- 画像処理&合成
- TFT出力
のステップを想定しています。結構なメモリが必要ですので、PSRAMがあるESP32-wroverシリーズが必須といえます。この場合は、GIFをダウンロードしたメモリ領域ごとデコーダに渡せるようにした方が便利なので実際はそちらの方で実装しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int GIF_MEM::init_decoder(uint8_t * gif, GIF_IMAGE * gifImage) { -略- } - 略 - /* int GIF_MEM::read_file(File fileObject, unsigned char *buf, int count){ return fileObject.read(buf,count); } */ int GIF_MEM::read_file(uint8_t * gif, unsigned char *buf, int count){ for(int i=0;i < count ; i++){ buf[i] = gif[gif_file_pos]; gif_file_pos++; } return 1; } |
重複するので全掲載は避けますが、上のような感じで変更します。gifはGIFをダウンロードしたPSRAM上のバッファを想定してます。こうすればSPIFFSやSDカード不要でPSRAM上で処理を完遂できます。