カメラと電子ペーパー
Spresense

概要

少し前にネットで見かけた、電子ペーパーを額装して飾るとやたらカッコいいを試してみたいと思い、Spresenseで試してみました。Spresenseカメラボードで撮影したJPEG画像を、電子ペーパーに表示する実験です。電子ペーパーは、電子ペーパーの実験で試した、Waveshare社の400ドットx300ドット、3色(白、黒、黄)の電子ペーパーモジュールを利用しました。

以下のことを行いました。

  1. 写真を撮る
  2. JPEGをビットマップに変換する
  3. ビットマップを電子ペーパーに表示する

このページでは、主にJPEGをビットマップに変換する部分を記載しています。

以下は、黄色の判断を強めに設定して、撮影したサンプルです。白と黒、黄色の3色しか表示できないので、色判断の閾値の設定が難しいです。この状態で他のものをとっても黄色が多くなりすぎます。

実験

写真を撮る

Spresense公式ページCamera チュートリアルCamera ライブラリを参考に以下のAPIを呼び出しました。theCameraは、事前に定義されているオブジェクト名です(Serialとかと同じです)。

API名 説明
theCamera.begin() カメラモジュールを初期化する。
theCamera.setAutoWhiteBalanceMode() 自動ホワイトバランス調整モードを設定する。
theCamera.setAutoISOSensitive() 自動ISO感度調整をする。
theCamera.setStillPictureImageFormat() 静止画写真の画像フォーマットを設定する。
theCamera.takePicture() 写真を撮影する。

JPEGをビットマップに変換する

JPEGデータからビットマップデータに変換するために、TJpgDec - Tiny JPEG Decompressorを利用しました。このライブラリは、小規模の組込みシステム向けに作成されていて、少メモリで動作するそうです。TJpgDec Module Application Noteに使い方が説明されています。

以下のAPIが提供されています。

API名 説明
jd_prepare() JPEGデータを解析して変換プロセスの準備を行う。
jd_decomp() JPEGイメージをRGBデータに変換する。

上記のAPIを利用するために、目的やJPEGデータの保存形態に沿った、以下の構造体と関数を作成する必要があります。

作成するもの 説明
IODEV構造体 以下のコールバック関数内で参照可能な構造体。
入力関数 JPEGデータを読みこむ。jd_prepare()から呼び出されるコールバック関数。
出力関数 ビットマップデータを取得する。jd_decomp()から呼び出されるコールバック関数。

IODEV構造体

入力関数と出力関数から参照可能な構造体です。この構造体は、TJpgDec自身が利用するわけではなく、以下の入力関数と出力関数で自分が利用するためのものです。

グローバル変数を利用するなどのときは、特になくても問題はありません。TJpgDec内では、void*型なので、IODEVという型名である必要もないようです。

今回は、入力関数で利用するために、以下の構造体を定義しました。

1
2
3
4
typedef struct {
  unsigned int dataRemain;  // the number of data remaining in the imageBuffer.
  uint8_t *imageBuffer;   // pointer to JPEG image buffer.
} IODEV;

入力関数

JPEGデータを読みこみ、TJpgDecライブラリに渡すための関数です。jd_prepare()から複数回呼び出されます。以下の形で定義します。

1
2
3
4
5
unsigned int in_func (
  JDEC* jdec,       /* Pointer to the decompression object */
  uint8_t* buff,    /* Pointer to buffer to store the read data */
  unsigned int ndata     /* Number of bytes to read */
);

JDECは、ライブラリが利用するデータを格納するための構造体です。jdec->deviceが、先ほど定義した、IODEV構造体へのポインタを保持しています。

この関数で必要な操作は以下の通りです。

  • buffがNULLでない場合は、JPEGデータの現在位置からndataバイト分のデータを、buffにコピーする。
  • buffがNULLの場合は、ndataバイト分のJPEGデータを読み飛ばす。
  • コピーしたデータもしくは読み飛ばしたデータのサイズ(基本的にはndata)を返却する。

出力関数

デコードされたビットマップデータを、取得するための関数です。jd_decomp()から複数回呼び出されます。以下の形で定義します。

1
2
3
4
5
int out_func (
  JDEC* jdec,    /* Pointer to the decompression object */
  void* bitmap,  /* RGB bitmap to be output */
  JRECT* rect    /* Rectangular region to output */
);

TJpgDecライブラリは、JPEGデータを一気にデコードするわけではなく、小さい矩形領域のデータを何回も返却するようです。矩形領域の情報は上記のrectに格納されています。JRECTは、以下のように定義されています。

1
2
3
typedef struct {
	uint16_t left, right, top, bottom;
} JRECT;

この矩形領域のビットマップデータが、左上から右下に向かって、bitmapに格納されています。ビットマップデータは、RGB888もしくはRGB565で、1ピクセル分のデータが、RGB888の場合は3バイト、RGB565の場合は1バイト単位で格納されています。

RGB888を利用するのかRGB565を利用するのかは、ヘッダファイル(tjpgd.h)内のJD_FORMATというマクロで制御します。0の場合RGB888、1の場合RGB565です。

出力関数では、このデータを自分の用途に合わせて加工していきます。

ビットマップを電子ペーパーに表示する

こちらは、電子ペーパーの実験の通りに行いました。Spresenseでも問題なく動作しました(ピン番号もデフォルトのままで動作しました)。

プログラム

接続

Spresenseと電子ペーパーモジュールは以下のように接続しました。

Spresenseのピン 意味 電子ペーパーモジュールのピン 備考
3.3V 電源 3.3V
GND GND GND
11 MOSI DIN SPI
13 SCK CLK SPI
10 CS CS
9 DC DC
8 RST RST
7 BUSY BUSY

プログラム

プログラムを以下に示します。利用していない関数も含まれています。

  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
#include <Camera.h>
#include <SPI.h>
#include <epd4in2b.h>
#include <epdpaint.h>
#include <stdio.h>
#include <string.h>
#include "tjpgd.h"

#define COLORED 0
#define UNCOLORED 1

const int imageSizeX = 320;
const int imageSizeY = 240;
const int displaySizeX = 400;
const int displaySizeY = 300;
const int szWork = 3100;

unsigned char bufferBlack[15000], bufferYellow[15000];
Paint paintBlack(bufferBlack, 400, 300);
Paint paintYellow(bufferYellow, 400, 300);
Epd epd;

typedef struct {
  unsigned int dataRemain;  // the number of data remaining in the imageBuffer.
  uint8_t *imageBuffer;   // pointer to JPEG image buffer.
} IODEV;

int checkBlack(uint8_t *data, uint8_t r, uint8_t g, uint8_t b) {
  if ((*data < r) && (*(data + 1) < g) && (*(data + 2) < b)) {
    return COLORED;
  }
  return UNCOLORED;
}

int checkBlackAverage(uint8_t *data, int threshold) {
  if ((*data + *(data + 1) + *(data + 2)) < threshold) {
    return COLORED;
  }
  return UNCOLORED;
}

int checkYellow(uint8_t *data, uint8_t r, uint8_t g, uint8_t b) {
  if ((*data > r) && (*(data + 1) > g) && (*(data + 2) < b)) {
    return COLORED;
  }
  return UNCOLORED;
}

unsigned int inFunc(JDEC *jd, uint8_t *buff, unsigned int nbyte) {
  IODEV *dev = (IODEV *)jd->device;

  if (nbyte > dev->dataRemain) {
    nbyte = dev->dataRemain;
  }
  if (buff) {
    memcpy(buff, dev->imageBuffer, nbyte);
  }
  dev->dataRemain -= nbyte;
  dev->imageBuffer += nbyte;

  return nbyte;
}

int outFunc(JDEC *jd, void *bitmap, JRECT *rect) {
  uint8_t *src;

  if (rect->left == 0) {
    printf("\r%lu%%", (rect->top << jd->scale) * 100UL / jd->height);
  }

  src = (uint8_t *)bitmap;
  for (int y = rect->top; y <= rect->bottom; y++) {
    for (int x = rect->left; x <= rect->right; x++) {
      if ((x < imageSizeX) && (y < imageSizeY)) {
        paintBlack.DrawPixel(x + (displaySizeX - imageSizeX) / 2, y + (displaySizeY - imageSizeY) / 2, checkBlack(src, 96, 96, 96));
        paintYellow.DrawPixel(x + (displaySizeX - imageSizeX) / 2, y + (displaySizeY - imageSizeY) / 2, checkYellow(src, 128, 128, 64));
      }
      src += 3;
    }
  }
  return 1;
}

void displayImage(CamImage *img) {
  JDEC jdec;
  void *work = (void *)malloc(szWork);
  IODEV devid;

  devid.dataRemain = img->getImgSize();
  devid.imageBuffer = img->getImgBuff();

  if (jd_prepare(&jdec, inFunc, work, szWork, &devid) == JDR_OK) {
    printf("Image size is %u x %u.\n%u bytes of work ares is used.\n", jdec.width, jdec.height, szWork - jdec.sz_pool);

    paintBlack.Clear(UNCOLORED);
    paintYellow.Clear(UNCOLORED);
    if (jd_decomp(&jdec, outFunc, 2) == JDR_OK) {
      printf("\rDecompression succeeded.\n");
      epd.ClearFrame();
      epd.SetPartialWindowBlack(paintBlack.GetImage(), 0, 0, paintBlack.GetWidth(), paintBlack.GetHeight());
      epd.SetPartialWindowRed(paintYellow.GetImage(), 0, 0, paintYellow.GetWidth(), paintYellow.GetHeight());
      epd.DisplayFrame();
    }
  }
  free(work);
}

void countdown(int seconds) {
  for (int i = seconds; i > 0; i--) {
    Serial.print(i);
    Serial.print(" ");
    delay(1000);
  }
  Serial.print("0");
}

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ;
  }

  if (epd.Init() != 0) {
    printf("epd.Init() failed.");
  }
  paintBlack.SetRotate(ROTATE_0);

  if (theCamera.begin(0, CAM_VIDEO_FPS_NONE) != CAM_ERR_SUCCESS) {
    printf("theCamera.begin() failed.\n");
  }

  if (theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_SHADE) != CAM_ERR_SUCCESS) {
    printf("theCamera.setAutoWhiteBalanceMode() failed.\n");
  }

  if (theCamera.setAutoISOSensitive(true) != CAM_ERR_SUCCESS) {
    printf("theCamera.setAutoISOSensitive() failed.\n");
  }

  if (theCamera.setStillPictureImageFormat(CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG) != CAM_ERR_SUCCESS) {
    printf("theCamera.setStillPictureImageFormat() failed.\n");
  }
}

void loop() {
  CamImage img;

  countdown(3);
  img = theCamera.takePicture();
  if (img.isAvailable()) {
    displayImage(&img);
  } else {
    printf("theCamera.takePicture() failed.");
  }
  delay(20000);
}

バージョン

Hardware:Spresense
Software:Arduino 1.8.13/Spresense Arduino Library 2.0.2

最終更新日

November 1, 2022

inserted by FC2 system