Arduino core ESP32 でJPEGデコードおよびエンコードしてみた

Arduino,core,ESP32内のライブラリでJPEGデコードおよびエンコードしてみた M5Stack

こんばんは。

今回は、Arduino core for the ESP32内に装備されているライブラリを使って、JPEG画像をビットマップ画像に変換(デコード)したり、逆にビットマップ画像をJPEG画像に変換(エンコード)したりする方法を私なりに紹介したいと思います。

ESP32に関しては、私は今までビットマップ画像しか扱ったことが無く、それでも充分かと思っていましたが、前回記事ではさすがにWiFi通信が重かったので、必要に迫られてJPEG画像を扱うことにしました。
そしたら、やっぱりJPEG圧縮した方が通信には断然有利だということが分かりました。

スポンサーリンク

実は、Twitterではつぶやいていましたが、ここのところずっと取り組んでいたイメージセンサOV2640とESP32とのWiFi動画ストリーミングで、ついにJPEGによる通信ができるようになったのです。
これについては次回以降の記事で紹介したいと思います。

ということで、イメージセンサのJPEGを扱う前に、まずはESP32側でJPEG画像のエンコード、デコードができなければなりません。

実は、最初、意気込んでJPEGデコード、エンコードライブラリを独自に一から作ろうと思ったのです。
が、調べれば調べるほど超超超難解ということが分かりました。
ハフマン符号化、ジグザグスキャン、等々、一から勉強しようすると時間がいくらあっても足りません。
パソコンやWeb上では、JPEG画像はあまりにも一般化しているのに、こんなにも難しい圧縮技術が盛り込まれていたとは思いませんでした。
素人の私にはお手上げです。
これが1990年代には既に確立されていたというのもまた驚きです。
自分のプログラミングなんて、結局は先人の知恵の恩恵を受けまくっているだけだと思いました。
一から創造できる物はもう何もないかもとすら思いましたね。

そんなこんなで、結局は先人にあやかって、ライブラリを使ってしまおうという結論に至りました。
人生、そんなもんです。

でも、JPEGファイルフォーマットについて、ごく浅くですが知識を得たので、個人的には大きな収穫でした。
これからはJPEG画像をガツガツ使っていきたいと思います。

ということで、Arduino core for the ESP32 に備わっているJPEG変換ライブラリを私なりに紹介します。
M5Stackライブラリが無くても利用できます。
ところで、私は素人なので誤っている可能性があります。
お気づきの点がありましたらコメント投稿等でご連絡いただけると助かります。

    【目次】

  1. 事前準備
  2. JPEGファイルの検出に使う先頭マーカーと末尾マーカー について
  3. Arduino core ESP32 内のライブラリでJPEGファイルをビットマップに変換(デコード)する
  4. ビットマップをJPEGファイルに変換 (エンコード)
  5. 実はM5Stackライブラリには超簡単なJPEGファイル表示ライブラリがある
  6. まとめ

事前準備

使ったもの

M5Stack Basic

Espressif Systems社のWiFi & Bluetooth付きマイコンESP32を搭載し、LCD(液晶ディスプレイ)、micro SDHCカードスロット、ボタンスイッチ、スピーカー、簡易バッテリー、GROVE端子を搭載した全部入りモジュールです。
ESP32で画像データをテストする場合にとっても手軽です。

(追記)
M5Stack Basicは、この記事を書いた当時より格段にバージョンアップしております。
以下のスイッチサイエンスさんの公式サイトをご参照ください。
https://www.switch-science.com/collections/%E5%85%A8%E5%95%86%E5%93%81/products/9010

Amazonに在庫が無い場合、スイッチサイエンスウェブショップにあると思います。

micro SDHCカード

JPEG画像ファイルを保存したり読み込んだりするテストに使用します。
M5Stackでは4GB~16GB までの容量が使用できます。
大手メーカー製のものを使用することをお勧めします。


Transcend microSDHCカード 8GB Class10 UHS-I対応 Nintendo Switch 動作確認済 TS8GUSDU1
トランセンドジャパン
¥1,080(2024/03/19 01:06時点)

その他パソコンおよびUSBケーブル等

ここでは、Windows 10で説明します。

Arduino core for the ESP32 のインストール

Arduino IDE に Arduino core for the ESP32 をインストールします。
動作確認しているバージョンは以下です。

Arduino IDE ver. 1.8.12
Arduino core for the ESP32 ver. 1.0.4

インストール方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

M5Stackライブラリのインストール

M5Stackを使う場合は、予めM5StackライブラリをArduino IDEにインストールしておいてください。
ver 0.3.0 で動作確認しています。
インストール方法は以下の記事を参照してください。

M5Stack ライブラリインストール方法 ( Arduino IDE 用)

サンプルのJPEGファイルをmicro SDHCカードに保存しておく

当方で画像編集ソフトを使って、以下のようなカラーバーのサンプルJPEG画像を作りました。
Windowsの場合、この画像を右クリックして、画像を保存できます。
これを micro SDHC カードのルートに保存しておきます。
フォーマットはFAT32です。
(micro SDHCカードのフォーマット方法はこちらを参照してください。)

ファイル名は
test_jpgdec.jpg
としておきます。
後ほどM5Stackで読み込みます。
(200 x 148 pixel)

[test_jpgdec.jpg]

テスト用カラーバーJPEGファイル

テスト用カラーバーJPEGファイル

JPEGファイルの検出に使う先頭マーカーと末尾マーカー について

先のサンプル用カラーバーJPEG画像ファイルをバイナリエディタで開くと、下図の様になります。
(Stirling使用)

先頭の 2byte が
0xFF, 0xD8
となっています。
16bitの場合は 0xFFD8 です。
これは、JPEGファイルの先頭を示すSOIマーカーというらしいです。
これをJPEGファイルの検出に使います。

次の 2byte が
0xFF, 0xE1 ( 0xFFE1 )
となっています。
0xE1 となっていれば、デジカメなどによるGPSデータなどが入っている EXIF情報が格納されているJPEGファイルだということが分かります。
この図ではPhotoshop で編集した EXIF形式のJPEGファイルということが分かりますね。
因みに、この4byte目が 0xE0、つまり0xFFE0を検出したら、JFIF形式のJPEGファイルだそうです。
JFIF形式はEXIF情報が入っていないものです。

では、JPEGデータの末尾を見てみると下図のようになります。

このように、末尾の 2byteが
0xFF, 0xD9 ( 0xFFD9 )
となっています。
これはファイルの終了を示す EOIマーカーというものらしいです。

ESP32等のマイコンでJPEGデータを抽出する際には、この末尾のバイトを検出してJPEGデータ1フレームと判断させます。

今回の場合、ファイル検出はライブラリ任せですが、イメージセンサのハードウェアからJPEG出力された信号で1フレームを検出する際には、このマーカーを利用します。
これについては次回以降の記事で紹介する予定です。

Arduino core ESP32 内のライブラリでJPEGファイルをビットマップに変換(デコード)する

実は、M5Stackライブラリを使ったり、外部ライブラリをインストールしたりしなくても、Arduino core for the ESP32 には予めJPEG変換ライブラリが入っているということが分かりました。
JPEG形式をビットマップ(BMP)に変換できさえすれば、LCD (液晶ディスプレイ)に表示させることができます。

いろいろ調べると、Arduino core for the ESP32内には、以下のフリーのオープンソースライブラリが使われていました。

ライブラリ名:TJpgDec – Tiny JPEG Decompressor
作者:ChaN氏
ライセンス:BSD
ソース:http://elm-chan.org/fsw/tjpgd/00index.html

このヘッダファイル tjpgd.h は、以下のパスにありました。
(Windows 10の場合)

C:\Users\自分のユーザー名\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\tools\sdk\include\esp32\rom

これが使えれば、汎用的に他のマイコンでも使えそうですね。
こういうのがオープンソースの醍醐味ですね。
感謝しなければなりません。

この使い方は、TJpgDecのサイト中のリンク、
TJpgDec Module Application Note
にサンプルコードがありました。

ただ、実際のArduino core for the ESP32内のヘッダファイル tjpgd.hでは、少々改変されています。
変数の型名が以下のような型名に再定義されていました。

/* These types must be 16-bit, 32-bit or larger integer */
typedef int				INT;
typedef unsigned int	UINT;

/* These types must be 8-bit integer */
typedef char			CHAR;
typedef unsigned char	UCHAR;
typedef unsigned char	BYTE;

/* These types must be 16-bit integer */
typedef short			SHORT;
typedef unsigned short	USHORT;
typedef unsigned short	WORD;
typedef unsigned short	WCHAR;

/* These types must be 32-bit integer */
typedef long			LONG;
typedef unsigned long	ULONG;
typedef unsigned long	DWORD;

何でこんな風に型を定義し直すのか、私にはまったく意味不明です。

サンプルスケッチ(プログラムソースコード)

とりあえず、それを考慮して、TJpgDec Module Application Noteにあるサンプルコードに習って、M5Stack用に私なりにプログラミングしてみました。

M5Stackに挿入されたmicro SDHCカードに、test_jpg1.jpg というファイルを予め保存しておき、それを読み込んでビットマップ(BMP)に変換して、M5Stackの LCD(液晶ディスプレイ)に表示させるものです。

M5Stackライブラリにはmicro SDHCカードとLCD表示を一括して制御する関数群も用意(後述しています)されていますが、TJpgDecライブラリの動作を理解するため、SDカード読み込みはM5Stackライブラリから分離しています。

サンプルコードを解読してみると、デコード後のビットマップフォーマットはRGB888でした。
つまり、1pixelが赤、緑、青のそれぞれ8bitずつの3byteで構成されているものです。
ただ、私の場合は表示速度やWiFi転送効率化のために RGB565フォーマット(1pixel: 2byte)にしたかったので、サンプルコードを以下のように書き換えてみました。
素人なので、間違えていたらスミマセン。

#include <rom/tjpgd.h> //TJpgDec (C)ChaN, 2012
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include <M5Stack.h>

const int cs = 4; //micro SD SPI CS
const char *path = "/test_jpgdec.jpg";

//#undef JD_FORMAT
//#define JD_FORMAT 1 //これはなぜか使えなかった。
/* User defined device identifier */
typedef struct {
  const void *fp; /* File pointer for input function */
  BYTE *fbuf;     /* Pointer to the frame buffer for output function */
  UINT wfbuf;     /* Width of the frame buffer [pix] */
} IODEV;

//************************************
void setup(){
  Serial.begin(115200);
  if(!SD.begin(cs)){
    Serial.println("Card Mount Failed");
    return;
  }

  File file = SD.open(path);
  if(!file){
    Serial.println("Failed to open file for reading");
    return;
  }
  Serial.printf("%s file size = %d\r\n", path, file.size());
  Serial.printf("1.Free Heap Size=%d\r\n", esp_get_free_heap_size());

  void *work;       /* Pointer to the decompressor work area */
  JDEC jdec;        /* Decompression object */
  JRESULT res;      /* Result code of TJpgDec API */
  IODEV devid;      /* User defined device identifier */
  devid.fp = &file;

  /* Allocate a work area for TJpgDec */
  work = malloc(3100);

  /* Prepare to decompress */
  res = jd_prepare(&jdec, in_func, work, 3100, &devid);
  if (res == JDR_OK) {
    /* Ready to dcompress. Image info is available here. */
    Serial.printf("Image dimensions: %u by %u. %u bytes used.\r\n", jdec.width, jdec.height, 3100 - jdec.sz_pool);

    devid.fbuf = (BYTE *)malloc(2 * jdec.width * jdec.height); //RGB565
    devid.wfbuf = jdec.width;

    res = jd_decomp(&jdec, out_func, 0);   /* Start to decompress with 1/1 scaling */
    if (res == JDR_OK) {
      /* Decompression succeeded. You have the decompressed image in the frame buffer here. */
      Serial.printf("\rOK  \n");
    } else {
      Serial.printf("Failed to decompress: rc=%d\n", res);
    }
  } else {
    Serial.printf("Failed to prepare: rc=%d\n", res);
  }

  free(work);             /* Discard work area */

  Serial.printf("2.Free Heap Size=%d\r\n", esp_get_free_heap_size());
  delay(500); //これはシリアルモニタへ正常に表示させるために必要。

  uint16_t width_bmp = jdec.width;
  uint16_t height_bmp = jdec.height;
  M5.begin();
  //M5.Lcd.setSwapBytes(true); //RGB<->BGR convert
  uint8_t X0 = 0, Y0 = 0;
  M5.Lcd.pushImage((int32_t)X0, (int32_t)Y0, (uint32_t)width_bmp, (uint32_t)height_bmp, (uint16_t*)devid.fbuf);

  if(devid.fbuf){
    free(devid.fbuf);
    devid.fbuf = NULL;
    Serial.println("Free devid.fbuf OK!");
  }else{
    Serial.println("Failed free devid.fbuf.");
  }
  Serial.printf("3.Free Heap Size=%d\r\n", esp_get_free_heap_size());
}

void loop(){

}
//**************************************
UINT in_func (JDEC* jd, BYTE* buff, UINT nbyte)
{
  UINT ret = 0;
  IODEV *dev = (IODEV*)jd->device;   /* Device identifier for the session (5th argument of jd_prepare function) */
  File *f2 = (File *)dev->fp;
  if (buff) {
    return (UINT)f2->read(buff, nbyte);
  } else {
    /* Remove bytes from input stream */
    if(f2->seek(nbyte, SeekCur)){ //seek mode:SeekCur=1(FS.h)
      ret = nbyte;
    }else{
      ret = 0;
    }
  }
  return ret;
}

UINT out_func (JDEC* jd, void* bitmap, JRECT* rect)
{
  IODEV *dev = (IODEV*)jd->device;
  uint8_t *src, *dst;
  uint16_t x, y, bwd;

  if (rect->left == 0) {
    Serial.printf("%lu%%\r\n", (rect->top << jd->scale) * 100UL / jd->height);
  }
  src = (uint8_t*)bitmap;
  //use RGB565
  dst = dev->fbuf + 2 * (rect->top * dev->wfbuf + rect->left);;
  //bws = 2 * (rect->right - rect->left + 1);
  bwd = 2 * dev->wfbuf;
  uint8_t msb, lsb, tmp1, tmp2;
  int16_t xcnt;
  for (y = rect->top; y <= rect->bottom; y++) {
    xcnt = 0;
    for (x = rect->left; x <= rect->right; x++){
      msb = *(src++) & 0b11111000;
      tmp1 = *(src++) & 0b11111100;
      msb = msb | (tmp1 >> 5);
      tmp2 = *(src++) & 0b11111000;
      lsb = (tmp1 << 3) | (tmp2 >> 3);
      *(dst + xcnt++) = msb;
      *(dst + xcnt++) = lsb;
    }
    dst += bwd;
  }

  return 1;    /* Continue to decompress */
}

【解説】

●1行目:
Arduino core for the ESP32 に予め装備されている、TJpgDec ライブラリをインクルードします。

●2-4行:
micro SDHCカードのファイル操作に必要なヘッダファイルです。

●5行目:
M5Stackライブラリのインクルードです。

●7行目:
M5Stackライブラリから分離してSDカード操作するので、ここでSPIインターフェースのCS(Chip Select ピン)を定義しておきます。
その他のMOSIやMISOピン等はデフォルト設定のままでOKなので、敢えて設定しません。

●8行目:
ここで micro SDHCカードのJPEG画像ファイルを指定します。
test_jpgdec.jpg というファイルは先ほど紹介したカラーバーJPEGファイルです。

●10-11行:
TJpgDec ヘッダファイルに、JD_FORMAT が定義されていて、ゼロならば、デコード後のビットマップフォーマットはRGB888。
1ならばRGB565 と記述されていました。
でも、なぜか JD_FORMAT=1 にしてもRGB888のままでした。
これは謎です。
ちゃんと動かないのでコメントアウトしています。

●13-17行:
デコードされたビットマップファイルを格納しておく構造体です。
fpは、SDカードのFILEハンドル等のポインタを指定したりします。
BYTE とか UINT 等の型は、先ほど説明したArduino core ESP32専用の謎の型定義です。

●42行:
work はJPEGをデコードする際に一時的に使う作業スペースのようです。
3100で確保すれば良いようです。

●45行:
ここでJPEGをデコードする準備をします。
90-106行の関数を使って、micro SDHCカードのJPEGファイルをまず2バイト読み込んで、先に紹介したSOIマーカ(0xFFD8)を検出して、JPEGファイルかどうかを判定しているようです。
その他、JPEGフォーマットの各マーカー等を検査しているようです。

●50行:
デコードしたビットマップデータをfbufに格納するための領域を確保します。
ここではRGB565フォーマットにするため、1pixelを2byteとして確保します。

●53行:
45行目の準備検査がOKならば、ここでJPEGファイルをビットマップに変換(デコード)します。
108-139行のout_func関数を数回呼び出して、画像をブロック毎に分割してビットマップに変換しているようです。

●66-74行:
67行目のdelayは、シリアルモニタの表示を正常化させるための遅延です。
74行目で M5Stack の LCD にビットマップを表示させています。

●90-106行:
45行目のjd_prepare関数によって読み込まれる関数です。
micro SDHCカードからJPEGファイルを読み込んでいます。
99行目のseek関数は、Arduino core ESP32用の関数です。
SeekCurは seek mode = 1 と定義されていて、ファイルの読込位置が保持されるモードです。

●108-139行:
53行のjd_decomp関数で呼ばれる関数です。
例えば、15 x 15 pixel のようにブロック単位でJPEG画像をビットマップに変換しているようです。
ここではRGB565形式に変換するので、119行や121行のように 1pixel 2バイトとしてポインタアドレスを移動させています。
124-136行では、TJpgDecのサンプルコードとは全く異なり、独自にRGB565用のコードに書き換えました。本当はもっと高速処理のコードがあるのですが、ここでは分かり易いように書いてみました。

コンパイル書き込み実行

では、先ほど紹介したカラーバーコードJPEGファイル(test_jpgdec.jpg)をmicro SDカードのルートにコピーしておき、M5Stackセットしておきます。
その後、これをコンパイル書き込み実行させてみます。
シリアルモニターを115200bpsで起動して、再起動すると、下図の様に表示されます。

JPEGデコード状況にあるように、JPEGデータを一気にビットマップにデコードしているわけではなく、ブロックごとに分割してデコードしている様子が分かります。

JPEGデコード前のヒープメモリサイズが
258,580 byte
で、JPEGデコード直後は
199,216 byte
となっています。
デコード後のビットマップ画像が200 x 148 pixelで、RGB565フォーマットですから、ビットマップ画像のサイズは、
200 x 2 x 148 = 59,200 byte
となりますので、ビットマップ(bmp)形式へデコードすると、かなりの容量のヒープメモリを食っていることが分かります。

そして、その後、ビットマップ(bmp)のメモリバッファを開放したら、
258,308 byte
となり、最初よりちょっと少なくなっていますが、メモリは概ね正常に元に戻っています。
JPEGデコードが終って、LCD表示させたら、早めにビットマップバッファを開放した方が良いですね。

M5StackのLCD(液晶ディスプレイ)に下図の様に表示されればOKです。

ビットマップをJPEGファイルに変換 (エンコード)

では今度は逆にビットマップ画像をJPEG形式に変換(エンコード)してみます。

因みに、私が長い間取り組んでいたM5CameraやOV2640等のイメージセンサは、ハードウェア内にJPEG出力が装備されているため、これから紹介する方法は不要です。

ただ、今後、JPEG出力の無いイメージセンサを使うことも有り得ますし、別のビットマップデータをJPEG圧縮してWiFi送信する機会が増えそうなので、ライブラリ等のソフトウェアでJPEGエンコードできた方が何かと便利なので、紹介したいと思います。

実はArduino core for the ESP32 には外部ライブラリをインストールしなくても、予め装備されていました。
その中のesp32-cameraライブラリにある、img_converters ライブラリを使います。
そのJPEGエンコードに限定してさらにソースを辿ってみると、オリジナルは以下のオープンソースライブラリでした。

ライブラリ名:jpeg-compressor
作者: Richard Geldreich 氏
ライセンス: Public Domain or Apache 2.0,
ソース: https://github.com/richgel999/jpeg-compressor

その readme を読んでみると、JPEGエンコードに使う関数は、
compress_image_to_jpeg_file
compress_image_to_jpeg_file_in_memory
を使うように書いてあります。

ただ、esp32-cameraライブラリでは改変されていて、compress_image_to_jpeg_fileという関数はありません。

実際にJPEGエンコードに使う関数は、
convert_image
fmt2jpg
等です。
この内、生成したJPEGファイルのサイズが返って来るのは、fmt2jpg 関数です。
特にJPEGファイルをWiFiでやり取りする場合には、ファイルサイズを取得する必要があるため、fmt2jpg関数が使い易いと個人的に思いました。
実際にサンプルスケッチのCameraWebServerでも使われていました。

サンプルスケッチ(プログラムソースコード)

では、ESP32でビットマップデータを生成して、fmt2jpg関数を使ってJPEGエンコードし、M5StackのLCD(液晶ディスプレイ)に表示させて、micro SDHC カードに保存するサンプルプログラム(スケッチ)を組んでみたので紹介します。
素人なので間違えていたらゴメンナサイ。

#include <M5Stack.h>
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include <img_converters.h> //Arduino core for the ESP32 1.0.4

const int cs = 4; //M5StackのSDカードスロットCSピン番号
const char *path = "/test1.jpg"; //エンコードしたJPEGをSDカードへ保存するファイル名
uint8_t *jpg_buf = NULL;
size_t jpg_buf_size;
uint8_t quality = 50; //JPEG圧縮品質(0~100)
const uint16_t width = 160; //サイズを大きくし過ぎるとリセットを繰り返すので注意
const uint16_t height = 160; //サイズを大きくし過ぎるとリセットを繰り返すので注意

uint32_t bmp_buf_size = 0;
uint8_t *bmp_buf = NULL;
uint8_t red, green, blue;

void setup() {
  Serial.begin(115200);
  Serial.println();

  //SRAMにbmp画像を描画するため、サイズが大きすぎるとリセットを繰り返すので注意
  bmp_buf_size = width * 3 * height; //RGB888
  bmp_buf = (uint8_t *)malloc(bmp_buf_size);
  Serial.printf("bmp buf size = %d bytes\r\n", bmp_buf_size);

  uint16_t bar_width = width / 4;
  uint32_t pt_count = 0;
  for(int j = 0; j < height; j++){
    //color white
    red = 0xff, green = 0xff, blue = 0xff;
    inputBmpBuf(red, green, blue, bar_width, bmp_buf, pt_count);

    //color red
    red = 0xff, green = 0x00, blue = 0x00;
    inputBmpBuf(red, green, blue, bar_width, bmp_buf, pt_count);

    //color green
    red = 0x00, green = 0xff, blue = 0x00;
    inputBmpBuf(red, green, blue, bar_width, bmp_buf, pt_count);

    //color blue
    red = 0x00, green = 0x00, blue = 0xff;
    inputBmpBuf(red, green, blue, bar_width, bmp_buf, pt_count);
  }

  //bmp画像をJPEGにエンコード
  if(fmt2jpg(bmp_buf, bmp_buf_size, width, height, (pixformat_t)PIXFORMAT_RGB888, quality, &jpg_buf, &jpg_buf_size)){
    Serial.println("fmt2jpg encord OK!!!");
    Serial.printf("jpeg size = %d\r\n", jpg_buf_size);
  }else{
    Serial.println("fmt2jpg failed.");
    return;
  }

  if(bmp_buf){
    free(bmp_buf);
    bmp_buf = NULL;
  }else{
    Serial.println("Failed free bmp_buf");
  }

  delay(500); //シリアルモニタへ正常に表示させるために必要
  M5.begin();

  //M5Stackライブラリを使う時、RGBが逆転してしまうため、以下のコマンドを使う
  M5.Lcd.writecommand(0x36); //ILI9341 Resister: Memory Access Control(0x36)
    M5.Lcd.writedata(0x00); //0x08 RGB->BGR convert

  uint16_t X0 = 0, Y0 = 0;
  M5.Lcd.drawJpg(jpg_buf, jpg_buf_size, X0, Y0);

  //以降、micro SDHCカードにJPEG画像保存
  if(!SD.begin(cs)){
    Serial.println("Card Mount Failed");
    return;
  }

  Serial.printf("Deleting file: %s\n", path);
  if(SD.remove(path)){
    Serial.println("File deleted");
  } else {
    Serial.println("Delete failed");
  }

  File file = SD.open(path, FILE_WRITE);
  if(!file){
    Serial.println("Failed to open file for reading");
    return;
  }
  if(file.write(jpg_buf, jpg_buf_size)){
    Serial.println("File written");
  } else {
    Serial.println("Write failed");
  }
  file.close();

  if(jpg_buf){
    free(jpg_buf);
    jpg_buf = NULL;
  }else{
    Serial.println("Failed free jpg_buf");
  }
}

void loop() {

}

void inputBmpBuf(uint8_t red, uint8_t green, uint8_t blue, uint32_t w, uint8_t *buf, uint32_t &pt_count){
  for(int i = 0; i < w; i++){
    buf[pt_count++] = red;
    buf[pt_count++] = green;
    buf[pt_count++] = blue;
  }
}

【解説】

M5StackのLCD(液晶ディスプレイ)のサイズは 320 x 240 pix ですが、そのサイズで作ってしまうと、ヒープメモリが足りなくなって、強制リセットを繰り返す事態に陥るので注意が必要です。
ここではとりあえず、160 x 160 pixel としてみました。

これも先ほど紹介したコードのように、LCD表示とmicro SDHCカード書き込みを分離したいので、全て統合化されたM5Stackライブラリ関数は極力使わないようにしました。

●2-4行:
micro SDHCカード書き込みに使用するための、Arduino core for the ESP32 の標準関数インクルード。

●5行目:
Arduino core for the ESP32 にあるimg_convertersライブラリのインクルード

●7行目:
micro SDHCカードスロットのCS(Chip Select)ピンだけ指定します。
MOSIピンやMISOピン等はSDカードライブラリのデフォルト設定のままでOKです。

●8行目:
エンコードしたJPEGファイルを保存しておくファイル名を決めておきます。

●11行目:
JPEG圧縮の品質を決めておきます。
0~100の範囲で決めます。低いほど圧縮率は高くなりますが、画質は劣ります。
今回の様な単色四角形だけなら品質の差は分かりませんが、複雑な画像になった場合は顕著に差が出ます。

●12-13行:
M5Stackの場合、320 x 240 pixel ですが、ビットマップ形式でRGB888形式のデータを確保すると、
320 x 3 x 240 = 230,400 = 230KB
という、膨大なSRAMメモリを消費してしまいます。
その容量でコンパイル書き込みすると、オーバーフローして強制リセットを繰り返すようになってしまいます。
ESP32 では容量が足りないため、ここでは画像サイズを160 x 160 pixel にしました。
それでも、
160 x 3 x 160 = 76,800 = 76KB
となってしまうので、SRAMがギリギリです。
また、4分割のカラーバーを作成しているため、widthは4で割り切れる値にしました。

●24-46行:
ここで、ESP32のSRAMに白、赤、緑、青の4色カラーバービットマップデータをRGB888形式で書き込みます。
実際に書き込む関数は、111-117行です。
28行目にあるように、カラーバーは4種類あるため、widthは4で割り切れる値にしないと、斜めって表示されるので注意です。

●49-55行:
ここで、Arduino core for the ESP32に装備されている fmt2jpg関数を使って、ビットマップ形式をJPEG形式にエンコードしています。
pixformat_t型はArduino core for the ESP32内の更にesp32-cameraライブラリの中の sensor.h で定義されています。
こんな感じです。

typedef enum {
    PIXFORMAT_RGB565,    // 2BPP/RGB565
    PIXFORMAT_YUV422,    // 2BPP/YUV422
    PIXFORMAT_GRAYSCALE, // 1BPP/GRAYSCALE
    PIXFORMAT_JPEG,      // JPEG/COMPRESSED
    PIXFORMAT_RGB888,    // 3BPP/RGB888
    PIXFORMAT_RAW,       // RAW
    PIXFORMAT_RGB444,    // 3BP2P/RGB444
    PIXFORMAT_RGB555,    // 3BP2P/RGB555
} pixformat_t;

●64-72行:
M5Stackライブラリを使って、LCD(液晶ディスプレイ)ILI9341またはILI9342Cに表示させます。
64行のdelayは、直前のシリアルモニター表示が間に合わないので、シリアルモニターのログを正常に表示させるためにdelay(500)にしています。

そして、最近流通しているM5Stack ( Basic および Fire )は、IPSタイプのディスプレイなので、色反転や、RGB表示がBGR表示されたりしてしまいます。
今回は、68-69行目のように、LCDドライバにレジスタコマンド0x36を送信して、その後の1byteデータでRGBとBGRを切り替えています。
69行目を0x08にすると、BGRに切り替わります。
68-69行が無いと、BGR表示になってしまいます。
色々試して実験してみて下さい。

●75-97行:
ここで、micro SDHCカードのルートに8行目で指定したファイル名でJPEGファイルを書き込んで保存しています。

●99-104行:
エンコードしたJPEGデータはまだSRAMに残っているので、これ以降使わなければfreeでメモリを開放することを忘れないようにしたいですね。
特に、イメージセンサからMJPEG で動画ストリーミングする場合は要注意です。

コンパイル書き込み実行

では、micro SDHCカードをM5Stackに入れて、コンパイル書き込み実行してみてください。
シリアルモニター(115200bpsで起動)には下図の様に表示されると思います。

quality(圧縮品質)が50の場合、
76,800 → 1,413 byte
まで圧縮できました。

100の場合は
76,800 → 1,934 byte

0の場合は
76,800 → 1,149 byte

でした。
かなり圧縮できましたね。

そして、M5StackのLCDには下図の様に表示されればOKです。

やはりJPEGエンコードって素晴らしい!!!
これなら、WiFi通信で気軽に転送できると思います。

因みに、この画像は単色カラーバーなので、圧縮率は高いです。
これが、複雑な画像、例えば風景写真ならば、あまり圧縮率は上がりません。
隣同士の画素が同色である範囲が多ければ多いほど圧縮率が上がるからです。

ところで、先にも述べましたが、OV2640等のJPEG出力のあるイメージセンサの場合は、ハードウェア内にJPEGエンコードを装備しているので、このライブラリを使う必要はありません。
これを使うのは、JPEG出力の無いイメージセンサを使う場合にはとても有効だと思います。

実はM5Stackライブラリには超簡単なJPEGファイル表示ライブラリがある

実は皆さんご存知だと思いますが、Arduino core の M5Stackライブラリには超簡単で使いやすいライブラリがあります。
micro SDHCに保存されたJPEGファイルをデコードしてRGBビットマップに変換して、次いでにLCD(液晶ディスプレイ)に表示まで、一行の関数でやってしまう優れものです。
こんな感じです。

#include <M5Stack.h>

const char *path = "/test_jpgdec.jpg";

void setup(void) {
  M5.begin();
  //JPEG画像をそのまま表示する場合
  M5.Lcd.drawJpgFile(SD, path);

  //JPEG画像を任意の座標位置で縮小表示させたい場合
  uint16_t X0 = 200, Y0 = 150;
  uint16_t width = 0, height = 0; //画像の幅と高さが不明な場合、値をゼロにしても良い
  uint16_t offset_x = 0, offset_y = 0;
  M5.Lcd.drawJpgFile(SD, path, X0, Y0, width, height, offset_x, offset_y, JPEG_DIV_2);
}

void loop() {

}

コンパイル実行する前に、micro SDHCカードのルートに先ほど紹介したカラーバーJPEG画像(ファイル名test_jpgdec.jpg)をコピーして保存しておき、それをM5Stackにセットからコンパイル書き込み実行してください。

これは超簡単で実に素晴らしいライブラリですね。
ビギナーにとってはとっても有難いです。

8行目だけでもM5StackのLCD(液晶ディスプレイ)に表示できますが、11-14行目のようにすると、画像を縮小(1/2、1/4、・・・)ができます。
widthやheightはJPEGファイル内のデータから取得できるので、ゼロでもOKです。
offsetは各自試してみて下さい。面白い効果が出ます。

実際には下図のように表示されます。

ただ、これでは、内部でどの様な処理をしているのかサッパリ分かりませんし、自作でガツガツとプログラミングしたい場合は自由度が無いので、私個人的にはライブラリを自作したい派ですね。

まとめ

今までビットマップデータで直にWiFi通信した方が何かと簡単かと思ったのですが、JPEG圧縮するとここまで容量が減ることが分かったので、やはり画像データの通信にはJPEG圧縮は必須だなと思いましたね。

そして、Arduino core for the ESP32 には予めJPEGデコーダ、エンコーダが装備されていることも分かりました。
これを利用しない手は無いですね。
そして、そのライブラリはオープンソースなので、他のマイコン環境に移植も期待できますね。

JPEG圧縮は難解で今まで敬遠していましたが、ライブラリを通してJPEGを扱うだけでも少々理解できた気がします。
今後はガツガツと利用していきたいと思いました。

以上、今回はここまでです。
次回はいよいよイメージセンサOV2640からハードウェアでJPEG出力させて、WiFiストリーミングする方法を紹介したいと思います。

ではまた・・・。

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/03/19 08:55時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥22,504(2024/03/18 15:19時点)
Excelでわかるディープラーニング超入門
技術評論社
¥2,068(2024/03/19 03:23時点)

コメント

タイトルとURLをコピーしました