ESP8266でGIF画像をデコードする

esp826611.jpg

JPEG画像に続いて、今度はGIF画像のデコードを試してみました。

GIF画像の圧縮・展開は、結構メモリが必要になります。
具体的には、最低でも25KB以上は必要です。

まず、GIFは最大256色を使いますが、色番号に対応する色はRGB各8ビットで表します。
従って256×3=768バイトを、カラーテーブルで使用します。

また、GIF画像は辞書を使う圧縮方式を使っています。
この辞書のエントリ数は最大で4096となっています。
1つの辞書エントリはコード3つ(そのエントリ自身と、次のエントリと前のエントリ)分のスペースを必要とします。
コードは最大12ビットですので、コード1つに2バイトを割り当てるとすると、辞書のためのメモリは約24KBほど必要になります。

なお、今回は使っていませんが、辞書を使わずに毎回復号コードを手続き的に求める方法を考案されている方もおられます。

ユニシス特許に抵触しないGIF画像のデコード

この方法を実装したLZ-Rというデコーダも上記ホームページで公開されています。

ESP8266はRAMを96KB積んでおり、Arduino環境で動かす場合は、そのうち80KB程度をユーザープログラムで使えますので、上記の辞書用メモリは何とか確保できそうです。

マイコンで使えそうなデコーダを、いろいろ既存のライブラリから探してみたのですが、意外と難航しました。
まずライブラリ自体が結構大きく多機能なものが多く、最低限の機能だけを実装したものがなかなか見当たりません。
また、デコードの方法も、展開後の画像全体を保持するバッファを渡すような形態が一般的です。
QVGA画像程度でも、全体を保持しようとすると320x240x3=230KBほどのRAMが必要となりますので、ESP8266ではこの方法はちょっと使えません。

ということで相当探し回った結果、Particleというマイコンボードのフォーラムで、使えそうな小さなデコーダを見つけました。
(ちなみに以前はParticleではなくSparkという名前でしたが、改名したようです。)

Parsing images from SD – General – Particle (formerly Spark)

このコードを元に、少々修正してESP8266で動くようにしました。
デコーダは別ファイルに分けており、メインのスケッチは以下のようになります。

#include <arduino.h>
#include <SPI.h>
#include <FS.h>
#include "gif.h"
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ILI9341.h> // Hardware-specific library

#define TFT_CS     15
#define TFT_RST    5
#define TFT_DC     4

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);

GIF gifdecoder;  


void setup() {
 File f;

 pinMode(TFT_RST, OUTPUT);
 digitalWrite(TFT_RST, LOW);
 digitalWrite(TFT_RST, HIGH);

 Serial.begin(115200);
 Serial.println("");

 tft.begin();
 // read diagnostics (optional but can help debug problems)
 uint8_t x = tft.readcommand8(ILI9341_RDMODE);
 Serial.print("Display Power Mode: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDMADCTL);
 Serial.print("MADCTL Mode: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDPIXFMT);
 Serial.print("Pixel Format: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDIMGFMT);
 Serial.print("Image Format: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDSELFDIAG);
 Serial.print("Self Diagnostic: 0x"); Serial.println(x, HEX);

 tft.fillScreen(ILI9341_BLACK);

 SPIFFS.begin();
 f = SPIFFS.open("/test.gif", "r");
 if (f) {
   Serial.println("File Open OK");
   gifDraw(f);
 }
}

void gifDraw(File f) {
 GIF_IMAGE g;
 
 gifdecoder.init_decoder(f, &g);
 Serial.print("Image Width: ");
 Serial.println(gifdecoder.imgWidth);
 Serial.print("Image Height: ");
 Serial.println(gifdecoder.imgHeight);

 tft.setAddrWindow(0,0,g.imageDesc.IMAGE_WIDTH-1, g.imageDesc.IMAGE_HEIGHT-1);
 gifdecoder.drawGIFImage(drawpixel);
}

void drawpixel(unsigned char r, unsigned char g, unsigned char b)
{
 tft.pushColor(tft.color565(r, g, b));
}


void loop() {
 // put your main code here, to run repeatedly:

}

SPIFFS上で、”/test.gif”というファイルをデコードしてILI9340ベースのTFT LCDに表示します。

デコーダに、ピクセル描画関数へのポインタを渡す構成にしていますので、デコーダ自体は表示デバイスには依存しません。(ファイルシステムのAPIには依存しています。)

QVGAサイズの画像を表示させたときの速度はこんな感じです。

ちなみにアニメーションGIFには対応していません。
アニメーションGIFについては、小さなアニメGIFをマイコンとマトリクスLEDで表示する「Aurora」というプロジェクトがありますので、そのデコーダが参考にできるかもしれません。

pixelmatix/aurora

Overview | SmartMatrix Animated GIF Player | Adafruit Learning System

ここで使われているのはTeensyというボードで、CPUはARMですがRAMは64KBと、ESP8266よりも若干貧弱なスペックです。

今回使ったデコーダライブラリ(修正後)も、ちょっと長いですがそのまま掲載しておきます。

gif.h:

#ifndef __GIF_H__
#define __GIF_H__

typedef void(*gif_draw_pixel_t)(unsigned char r, unsigned char g, unsigned char b);

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 R;
unsigned char G;
unsigned char B;
} COLOR;

typedef struct{
SCREEN_DESC screenDesc;
IMAGE_DESC imageDesc;
} GIF_IMAGE;

typedef struct{
unsigned short code;
unsigned short prevCode;
unsigned short nextCode;
} DICTIONARY_ENTRY;

class GIF {
private:
unsigned char BUFFER[16];
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;

void read_file(File, unsigned char *, int);
unsigned char read_code;

unsigned char byte5;
unsigned char byte9;
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

gif.cpp:

#include <FS.h>
#include "gif.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;

	read_file(g_fileObject, BUFFER, 6);
	if (checkSignature((char*)BUFFER) != 1) return -1;
 
	read_file(g_fileObject, BUFFER, 7);

	(*gifImage).screenDesc.SCREEN_WIDTH = (BUFFER[1] << 8) + BUFFER[0];
	(*gifImage).screenDesc.SCREEN_HEIGHT = (BUFFER[3] << 8) + BUFFER[2];
	(*gifImage).screenDesc.BYTE_5 = BUFFER[4];
	(*gifImage).screenDesc.BACKGROUND = BUFFER[5];
	(*gifImage).screenDesc.ASPECT_RATIO = BUFFER[6];

	byte5 = (*gifImage).screenDesc.BYTE_5;

	bpp = (byte5 & 0x07);

	GCT_size = 2 << bpp;

	if (byte5 & 0x80){

		for (i = 0; i < GCT_size; i++){
			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;

	do {
		read_file(g_fileObject, BUFFER, 1);
	} while (BUFFER[0] != 0x2C);

	read_file(g_fileObject, BUFFER, 11);

	(*gifImage).imageDesc.LEFT = (BUFFER[1] << 8) + BUFFER[0];
	(*gifImage).imageDesc.UP = (BUFFER[3] << 8) + BUFFER[2];
	(*gifImage).imageDesc.IMAGE_WIDTH = (BUFFER[5] << 8) + BUFFER[4];
	(*gifImage).imageDesc.IMAGE_HEIGHT = (BUFFER[7] << 8) + BUFFER[6];
	(*gifImage).imageDesc.BYTE_9 = BUFFER[8];

	imgWidth = (*gifImage).imageDesc.IMAGE_WIDTH;
	imgHeight = (*gifImage).imageDesc.IMAGE_HEIGHT;
	byte9 = (*gifImage).imageDesc.BYTE_9;

	minLZWCodeLength = BUFFER[9] + 1;
	currentDataSectionLength = BUFFER[10];

	code_CLEAR = GCT_size;
	code_END = GCT_size + 1;
	PrimaryDictSize = GCT_size + 2;
	PrimaryCodeSize = minLZWCodeLength;
	currentCodeSize = minLZWCodeLength;

	return 0;
}

void GIF::read_file(File fileObject, unsigned char *buf, int count){
 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, &currentDataSectionLength, 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, &currentDataSectionLength, 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;

	rgbColor = g_GlobalColorTable[code];
	(*gif_draw_pixel)(rgbColor.R, rgbColor.G, rgbColor.B);
}

コメント

  1. 野比須十四六 より:

    いつも刺激的でわかりやすい解説ありがとうございます。これからも勉強させてください。

  2. 匿名 より:

    コメントありがとうございます! このブログ、果たして読まれているんだろうか・・・と気になっていたところでした。