UNO R4 WiFiとESP32でREST API (3) - アイコン画像をUNO R4のFlash容量内に収める方法
はじめに
Bodmer/OpenWeather を Arduino UNO R4 WiFi でも動作するようにマイグレーションした ArduinoOpenWeather を紹介する3部作の最終回です。
- RTC、NTP、HTTP、TLS、ルートCA証明書の例題
- 大きなJSONデータを解析するArduinoJson活用法
- アイコン画像をUNO R4のFlash容量内に収める方法(今回)
オリジナル版のお天気アイコンは、Flash 上に構築されたファイルシステムにアップロードした1画素24ビットの .bmp ファイルを SPIFFS(現在は非推奨)で読み出し描画していますが、当然、UNO R4 では容量オーバーです。
そこで今回は、如何にコンパクトな画像フォーマットで同等の機能を実現するかがお題となります。
白羽の矢 を立てたのが、ESP32 だけでなく UNO R4 でも動作するグラフィックライブラリ moononournation/Arduino_GFX です。ESP32 では描画速度こそ lovyan03/LovyanGFX や Bodmer/TFT_espi に譲りますが、UNO R4 では Adafruit-GFX-Library よりずっと高速で、かつ少ないメモリのマイクロコントローラーでも動作するように作られた、私のお気に入りのライブラリの1つです。
Arduino_GFX で扱えるビットマップデータ
Arduino_GFX における「ビットマップ」とは、いわゆる Windows の .bmp 形式 ではなく、画素の情報が配列として並んだ単純なデータ列を指します。色深度 に応じた描画メソッドのうち、主なものを以下に紹介します。
-
RGB888 カラー
draw24bitRGBBitmap()は、RGB888 のビットマップデータを RGB565 に変換して描画します。 -
RGB565 カラー
draw16bitRGBBitmap()は、RGB565 のビットマップデータを描画します。またその派生として、透過色が指定可能なdraw16bitRGBBitmapWithTranColor()、1画素1ビットの切り抜き用マスク画像が指定可能なdraw16bitRGBBitmapWithMask()があります。画像形式ごとのエンディアンの違い に対応するため、名前にBeの入った ビッグエンディアン用メソッド もあります。 -
インデックスカラー
drawIndexedBitmap()は、RGB565 のカラーパレットを参照するビットマップデータを描画します。データサイズは RGB565 カラーの + カラーパレット(最大 512 バイト)になります。 -
グレースケール
drawGrayscaleBitmap()は、1画素1バイトのグレースケール画像を描画します。データサイズは RGB565 カラーのちょうど になります。RGB565 に丸められるため 256 階調分の濃淡は再現できません。 -
モノクローム
drawBitmap()は、1画素1ビットのビットマップデータを描画します。データサイズは RGB565 のおよそ です。描画色や背景色の指定も可能です。drawXBitmap()では、GIMP で.xbmファイル にエクスポートした C ソースコードの利用が可能です。 -
GFXfont
drawChar()は、GFXfontで定義される1画素1ビットのビットマップデータを描画します。アイコンフォントを組み込めば、画像の代わりになります。
実例
スプラッシュ画像(インデックスカラー画像)
今回のアプリでは、起動時の初期化中に OpenWeather のロゴ画像 を表示しています。ご覧の通り色数が少なく、RG565 カラーや JPEG よりインデックスカラーが適しています。
JPEG の利用について
画像サイズが大きい場合、JPEG の利用も考えられます。以下の2つのライブラリが有名どころでしょう。
前者はおよそ 20KB の RAM を要求し、シンプルな adafruit_gfx_demo ですら UNO R4 ではメモリ不足でコンパイルが通りません。
また後者は Flash_Jpg_GFX を参考にしましたが、グローバル変数 TJpgDec が常に 3.5KB の作業用メモリをキープし、もれなく SD もリンクされるため、今回のアプリでは JSON データの解析時にメモリ不足に陥ってしまいました。
今回のように一時的に表示するだけの場合は、変数はローカル化し、不要になった作業用メモリは解放されるよう TJpgDec を直接組み込むのが良いと思います。
ちなみに JPEG 画像の Flash 埋め込み用データを作成するには、ファイルの中身を 16 進数でダンプしてくれる File to hexadecimal converter が便利です。
組み込み向けに LCD 用の画像変換ツールは幾つもありますが、インデックスカラーは見つからず、Arduino_GFX と親和性の良い、独自の変換プロセスを作らざるを得ませんでした 😉
インデックスカラー化したビットマップデータの作成方法
画像のインデックスカラー化には GIMP を使います。
まずはメニューから「画像 → モード → インデックス…」を選択します。
色数が多い場合には、ディザリングに誤差拡散系を指定した方が良いようです。詳しくは GIMP のマニュアル 「インデックスカラーに変換」ダイアログ を参照して下さい。
.h にして出力次に「ファイル → 名前を付けてエクスポート…」を選択し、「画像のエクスポート」ダイアログで「ファイル形式の選択」に「C ソースコードヘッダー」(拡張子は .h)を選択します。
さらにエクスポートした .h ファイルのカラーパレットを rgbconvert.py で RGB888 から RGB565 に書き換えます。
rgbconvert.py の使用方法
バージョン 2.7.16 以上の Python で、コマンドラインから以下を実行します。
% python rgbconvert.py input.h > output.hWindows な人向けの PowerShell スクリプト
rgbconvert.py は Mac/Linux を想定し、以下を要件に ChatGPT に作らせました。
| 対象 | 変換内容 |
|---|---|
| 入力ファイル | GIMP でエクスポートした C ソースコードのヘッダーファイル |
| 変換結果 | 標準出力に表示 |
width / height |
変数宣言を #define 文に変換 |
HEADER_PIXEL マクロ |
マクロ定義のブロックを削除 |
header_data_cmap |
RGB888の各色をRGB565に変換し、uint16_t 配列に変更 |
header_data |
画素データを16進数表記に変換し、uint8_t 配列に変更 |
| 変数名 | 入力ファイル名から拡張子を除き、変数名に使用可能な文字列に置換 |
| ヘッダー全体 | 入力ファイル名に基づくインクルードガードを追加 |
| エラー処理 | 標準エラー出力に適切なエラーメッセージを表示 |
Windows 10/11 用に PowerShell で実行可能な rgbconvert.ps1 も作らせましたが、私の Win 環境は Mac 上の Win 7 と古いので、動作は未検証です。
実行は、以下のように プロファイルは読み込まず、ExecutionPolicy を一時的に無視せよ とのことですが、組織のグループポリシーによってはエラーになるそうです。
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\rgbconvert.ps1" ".\input.h" ".\output.h"実行は自己責任でお願いします 🙇♂️
変換前 (input.h)
GIMP でエクスポートした .h の中身はおよそ次のようになっています。
/* GIMP header image file format (INDEXED): input.h */
static unsigned int width = 180;
static unsigned int height = 80;
/* Call this macro repeatedly. After each use, the pixel data can be extracted */
#define HEADER_PIXEL(data,pixel) {\
pixel[0] = header_data_cmap[(unsigned char)data[0]][0]; \
pixel[1] = header_data_cmap[(unsigned char)data[0]][1]; \
pixel[2] = header_data_cmap[(unsigned char)data[0]][2]; \
data ++; }
static unsigned char header_data_cmap[256][3] = {
{ 58, 57, 60},
...
{255,255,255}
};
static unsigned char header_data[] = {
254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,
...
254,254,254,254
};変換後 (output.h)
変換後は、RGB888 のカラーパレットが RGB565 に変換され、変数名も入力ファイル名を基に置換されます。
#ifndef _INPUT_H_
#define _INPUT_H_
/* GIMP header image file format (INDEXED): input.h */
#define INPUT_WIDTH 180
#define INPUT_HEIGHT 80
static const uint16_t input_cmap[256] = {
0x2966, 0x31A6, 0x39C7, 0x39C7, 0x39C7, 0x39E7, 0x39E7, 0x39E8,
...
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
};
static const uint8_t input[] = {
0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
...
0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE
};
#endif描画方法
変換後の .h をインクルードし、drawIndexedBitmap() に変換結果を与えます。
gfx->drawIndexedBitmap(
0, 0, // 描画開始位置
const_cast<uint8_t *>(input), // ビットマップデータ
const_cast<uint16_t *>(input_map), // カラーパレット
INPUT_WIDTH, INPUT_HEIGHT // 画像のサイズ
);月齢画像(グレースケール画像)
皆さんは、月の満ち欠けって興味ありますか? 私は大いにアリなので是非とも移植したい項目でした 🎑
ということで、月齢画像は NASA の Moon Phases Loop から切り出しました。
Flash 埋め込み用ビットマップデータの作成には、ブラウザ上で動作する軽量な File to C style array converter が私のお気に入りです。
ほぼそのまま使える C ソースコードが生成できます。
月齢の計算アルゴリズムについて
Google で 月齢の計算アルゴリズム を検索すると AI の解説を含めて様々出てきます。そこで Bodmer 氏が採用した 月齢計算プログラム との精度を比較してみました。「正解」としたのは以下の2つです。
結果、簡易計算や近似計算では数日のズレが出るのに対し、Bodmer 氏のプログラムでは大体 ±0.5 日未満のズレに収まっていました。このプログラムの出展を見つけるのに苦労しましたが、インターネットアーカイブに残っていました。詳しいロジックは分かりませんが、ざっと眺めてみると、地球から見た太陽と月との位置関係を元に幾何学的に計算しているように思います(定かではありませんが… 🙄)。
ということで、これをそのまま使わせてもらうことにしました。
ちなみに後日 Auduino ライブラリの signetica/MoonPhase を見つけましたが、「正解」とのズレは未検証です。
お天気アイコン(GFX フォント画像)
Arduino_GFX のフォント ArduinoFreeFontFile は、Adafruit の GFX フォント を踏襲しています。
カスタムなフォントを作るには、TrueType フォントを変換するプログラム fontconvert か、オンラインツールの truetype2gfx が利用できます。前者は生成するアスキーコードの範囲が指定でき、後者は 0x20 〜 0x7E までのフォントが定義された .h ファイルをゲット出来ます。
fontconvert のコンパイル方法
fontconvert.c のコンパイルには FreeType が必要です。
基本的には、Download から最新版のパッケージをダウンロード&展開し、docs/INSTALL から各 OS 環境ごとのコンパイル&インストール方法に従いますが、私は Google に聞いた「各OS環境におけるfreetypeのインストール方法」が簡単で上手くいきました 😁
私は SVG は全くのシロウトなので、ベースの TrueType フォント を FontForge に読み込み、「アウトラインウィンドウ」の GUI 上で編集を行いました。
Google font のアイコンフォント から SVG ファイルをダウンロードし、同ウィンドウから「ファイル(F) → 取り込み(I)…」も可能です(位置の合わせ込みが必要ですが…)。
FontForge の日本語ドキュメント が有志により整備されているので、一読推奨です 😅
アイコンのカラー化
フォントの描画は基本的に単色ですが、オリジナルのカラー化された GUI に近づけるため、複数のパーツに分けたフォントを同じ位置に異なる色で重ね打ちする「多色刷り」を実装してみました。
興味があれば gfx.h の ココ と gfx.cpp の ココ や ココ を読み解いてみて下さい 😎
参考情報
フォント関連
お天気アイコンの例
- Google fonts のお天気アイコン
- kickstandapps/WeatherIcons
- erikflowers/weather-icons
- boxbot6/easy-weather-icons-font
おわりに − ヒープメモリ残量の見積もり
本記事のメインテーマは Flash サイズでしたが、今後「お天気アイコンをタッチすると、時刻ごとの天候や気温のグラフを表示する機能」が追加できそうか、UNO R4 WiFi でヒープメモリの残量を見積もってみました。
見積もり方法は前回記事「3. deserializeJson() が使えるヒープメモリを確保する」の通りです。前提となるソフトウェアのバージョンは以下となります。
| パッケージ/ライブラリ | バージョン |
|---|---|
| ArduinoCore-renesas | 1.6.0 |
| ArduinoJson | 7.4.3 |
| GFX Library for Arduino | 1.6.9 |
| NTPClient | 3.2.1 |
| RTC | ArduinoCore-renesas 1.6.0 |
| SPI | ArduinoCore-renesas 1.6.0 |
| WiFiS3 | ArduinoCore-renesas 1.6.0 |
| Wire | ArduinoCore-renesas 1.6.0 |
まずはコンパイル結果ですが、静的状態では RAM 容量にまだまだ余裕があります。
Sketch uses 209684 bytes (79%) of program storage space. Maximum is 262144 bytes.
Global variables use 10408 bytes (31%) of dynamic memory, leaving 22360 bytes for local variables. Maximum is 32768 bytes.
マップファイルから求めたヒープの残りサイズもコンパイル結果に近い値です。
| 項目 | アドレス/サイズ |
|---|---|
__HeapLimit |
0x0000000020007b00 |
__HeapBase |
0x00000000200028c0 |
__HeapLimit - __HeapBase |
0x5240 (21056) |
次に動的状態で調べるため、setup() の初期化終了時点と loop() の末尾に次のコードを仕掛け、ヒープメモリの使用状況を表示させました。
#include <stdio.h>
#include <malloc.h>
void check_heap_size(void) {
struct mallinfo mi = mallinfo();
printf("total space allocated from system (arena): %lu\n", mi.arena);
}ということで、ヒープメモリ残量の見積もり結果は以下の通りとなりました。
| 計測ポイント | arena のサイズ |
(__HeapLimit - __HeapBase) - arena |
|---|---|---|
setup() の初期化終了時点 |
1484 | 21056 - 1484 = 19572 (19.0KB) |
loop() の末尾 |
17588 | 21056 - 17588 = 3468 (3.0KB) |
う〜ん、残り 3KB でどれだけできるか分かりませんが、そのうちチャレンジしてみます 🍁