デジカメ弐号機 第4回 ストリーミング
2022.11.18
YouTubeでポイントを説明しています。画像をクリックすると再生できます。
第4回では、ArduCAM カメラモジュールを用いたストリーミング画像を、第3回で紹介したディスプレイに表示します。
■ArduCAM Mini 2MP カメラモジュール
ArduCam Mini 2MP は、オムニビジョン OV2640 2Mピクセルレンズを搭載したカメラモジュールです。
3Mb(384KB)のフレームバッファを搭載しています。レンズマウントはM12マウントで交換も可能です。
【主な仕様】
| 電源電圧: | DC5V |
| 消費電流: | ノーマル70mA、ローパワーモード20mA |
| フレームバッファ: | 384KB |
| 出力解像度: |
1600×1200、800×600、
640×480、320×240、
352×288、176×144 |
| 出力形式: | RAW、YUV、RGB、JPEG |
| 基板サイズ: | 34×24mm |
| サイズ: | 40×24×33mm(厚みはレンズに依存) |
ArduCamカメラモジュールは、秋月電子通商で購入しました。

ArduCAM Mini 2MP ピン配置

ArduCAM とディスプレイ、マイコンボードとの接続は下記のようになります。
| ST7789 | - | QT Py ESP32S2 | - | ArduCam |
| | | A0 | - | CS |
| D/C | - | A1 | | |
| TCS | - | A2 | | |
| MOSI | - | MOSI | - | MOSI |
| | | MISO | - | MISO |
| SCK | - | SCK | - | SCLK |
| GND | - | GND | - | GND |
| Vin | - | 3.3V | - | +5V |
| | | SDA | - | SDA |
| | | SCL | - | SCL |
前面、カメラモジュール側からの配線の様子です。

背面、ディスプレイ側からの配線の様子です。

■プログラミング
ソースコードのビルドには、PlatformIOを使用しています。
Arduino開発環境構築 PlatformIO
ビルドに必要なファイル
ビルドに必要なライブラリに関しては、設定ファイル(platform.ini)の lib_deps = に指定してもよいのですが、GitHubからライブラリをダウンロード、展開して、必要なファイルをソースと同じディレクトリに置いてビルドしています。
Arduino/ArduCAM
┣ ArduCAM.cpp
┣ ArduCAM.h
┣ memorysaver.h
┣ ov2640_regs.h
┗ <examples>
┗ <mini>
┗ <ArduCAM_Mini_Video_Streaming>
┗ ArduCAM_Mini_Video_Streaming.ino
adafruit/Adafruit_BusIO
┣ Adafruit_I2CDevice.cpp
┗ Adafruit_I2CDevice.h
$ vi src/memorysaver.h
ファイルを開くと、デフォルトで OV5642_MINI_5MP_PLUS が定義されているので、コメントアウトして、OV2640_MINI_2MP を有効にします。
//Step 1: select the hardware platform, only one at a time
//#define OV2640_MINI_2MP
#define OV5642_MINI_5MP_PLUS
↓
#define OV2640_MINI_2MP
//#define OV5642_MINI_5MP_PLUS
デジカメ初号機では、サンプルコード ArduCAM_Mini_Capture2SD.ino を編集して、撮影画像をJPEG形式で保存していました。
弐号機では、撮影画像をBMP形式で保存するので、ArduCAM_Mini_Video_Streaming.ino を用います。
キャプチャーもストリーミングも、連続撮影していることに変わりないのですが、2つのサンプルコードの大きな違いは下記の箇所です。
ArduCAM_Mini_Capture2SD.ino 抜粋
myCAM.set_format(JPEG);
~~~~
while ( length-- ) {
temp = SPI.transfer(0x00);
if ( (temp == 0xD9) && (temp_last == 0xFF) ) {
buf[i++] = temp; //save the last 0XD9
myCAM.CS_HIGH();
outFile.write(buf, i);
outFile.close();
} else if ((temp == 0xD8) & (temp_last == 0xFF)) {
buf[i++] = temp_last;
buf[i++] = temp;
}
}
上記のJPEG保存用のサンプルコードでは、予めJPEG画像形式に変換された状態で、画像情報が提供されてしまいます。
これをディスプレイに表示可能なRGB形式に変換するのは、かなり複雑なコードを追加する必要があります。
ArduCAM_Mini_Video_Streaming.ino 抜粋・編集
color565 = (uint16_t*)malloc(240*240*2);
myCAM.set_format(BMP);
uint16_t *imgpt;
~~~~
SPI.transfer(0x00);
char VH, VL;
int y = 0, x = 0;
int col = 0;
imgpt = color565;
for (y=0; y<240; y++) {
imgpt += 240;
for (x=0; x<320; x++) {
VH = SPI.transfer(0x00);
VL = SPI.transfer(0x00);
if ((x>39)&&(x<280)) {
imgpt--;
*imgpt = VH|(VL<<8);
}
delayMicroseconds(12);
}
imgpt += 240;
}
Arduino では、RAMが小さいため、画像データをメモリー上にバッファリングすることはできませんが、
Adafruit QT Py ESP32-S2 は、2MバイトのPSRAM を実装しているため、
画像の一次保管場所として、メモリー空間を確保することができます。
color565 = (uint16_t*)malloc(240*240*2);
ArduCam で画像形式をBMPに設定すると、color565(赤:5ビット、緑:6ビット、青:5ビット)の
2バイトの画像データとして取得することができます。
この上位1バイトと下位1バイトをメモリー上に保存します。
*imgpt = VH|(VL<<8);
ArduCam から得られた画像の表示用ディスプレイとして、縦横240ピクセルのLCDを使用しているので、
横長の画像の中心部分の幅240ピクセル分を切り取って使います。
if ((x>39)&&(x<280)) {
また、撮影画像が左右反転していたので、再度反転させて正常な画像に戻しています。
imgpt += 240;
保存した画像を表示する部分は、前回第3回 DISPLAYのSPI制御で説明していますが、下記のようになります。
void show() {
spi_lock(LOCK_ST7789);
drawRGBBitmap(0,0,color565,240,240);
spi_lock(LOCK_NONE);
}
SPI クロック速度
ArduCAM SPIインターフェイスでは、SPI MODE0 固定です。SCLK は最大 8MHzになります。
SPISettings settingsArduCam(8000000, MSBFIRST, SPI_MODE0);
SPISettings settingsST7789(24000000, MSBFIRST, SPI_MODE3);
下記のコードでは、サンプルソースの余計なコードを削除、ロジックの追加などを行っています。
$ vi src/ArduCAM_Mini_Video_Streaming.ino
#include <Wire.h>
#include <SPI.h>
#include <ArduCAM.h>
#include "memorysaver.h"
#define SPI_SCK SCK
#define SPI_MISO MISO
#define SPI_MOSI MOSI
#define SPI_RST -1
#define SPI_DC A1
#define SPI_ARDUCAM_CS A0
#define SPI_TFT_CS A2
ArduCAM myCAM( OV2640, SPI_ARDUCAM_CS );
typedef struct {
uint32_t Width;
uint32_t Height;
} IMAGE_INFO;
SPISettings settingsArduCam(4000000, MSBFIRST, SPI_MODE0);
SPISettings settingsST7789(24000000, MSBFIRST, SPI_MODE3);
#define LOCK_NONE 0
#define LOCK_ARDUCAM 1
#define LOCK_ST7789 3
void spi_lock(int lock) {
static int status = -1;
if (lock==LOCK_NONE) {
SPI.endTransaction();
status = -1;
return;
}
if (lock!=status) {
SPI.endTransaction();
if (lock==LOCK_ARDUCAM) {
SPI.beginTransaction(settingsArduCam);
} else if (lock==LOCK_ST7789) {
SPI.beginTransaction(settingsST7789);
}
status = lock;
}
}
#define TFT_WIDTH 240
#define TFT_HEIGHT 240
uint16_t *color565;
#define HBYTE(u) ((u >> 8) & 0xFF)
#define LBYTE(u) (u & 0xFF)
// TFTにコマンドを送信
void tftSendCommand(uint8_t command) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
}
// TFTにコマンド+1バイトデータを送信
void tftSendCommand1(uint8_t command, uint8_t data1) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
digitalWrite(SPI_TFT_CS, HIGH);
}
// TFTにコマンド+2バイトデータを送信
void tftSendCommand2(uint8_t command, uint8_t data1, uint8_t data2) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
SPI.transfer(data2);
digitalWrite(SPI_TFT_CS, HIGH);
}
// TFTにコマンド+3バイトデータを送信
void tftSendCommand3(uint8_t command, uint8_t data1, uint8_t data2, uint8_t data3) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
SPI.transfer(data2);
SPI.transfer(data3);
digitalWrite(SPI_TFT_CS, HIGH);
}
// TFTにコマンド+4バイトデータを送信
void tftSendCommand4(uint8_t command, uint8_t data1, uint8_t data2, uint8_t data3, uint8_t data4) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
SPI.transfer(data2);
SPI.transfer(data3);
SPI.transfer(data4);
digitalWrite(SPI_TFT_CS, HIGH);
}
void drawRGBBitmap(uint16_t x, uint16_t y, uint16_t *rgbs, uint16_t width, uint16_t height) {
uint8_t h, l;
width--;
h = (uint8_t)(width>>8);
l = (uint8_t)(width&0x00ff);
tftSendCommand4(0x2A,x,y,h,l); // Colmun Address
height--;
h = (uint8_t)(height>>8);
l = (uint8_t)(height&0x00ff);
tftSendCommand4(0x2B,x,y,h,l); // Row Address
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(0x2C);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(rgbs, width*height*2);
digitalWrite(SPI_TFT_CS, HIGH);
digitalWrite(SPI_DC, LOW); // Command mode
}
// 表示開始ライン設定
void dispStartLine(uint16_t y) {
uint8_t yH = (y >> 8) & 0xFF ;
uint8_t yL = y & 0xFF ;
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(0x37);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(yH);
SPI.transfer(yL);
digitalWrite(SPI_TFT_CS, HIGH);
}
void init_tft() {
spi_lock(LOCK_ST7789);
// --- HARD Ware Reset
if (SPI_RST >= 0) {
digitalWrite(SPI_RST, HIGH);
delay(500); // VDD goes high at start, pause for 500 ms
digitalWrite(SPI_RST, LOW); // Bring reset low
delay(100); // Wait 100 ms
digitalWrite(SPI_RST, HIGH); // Bring out of reset
delay(500); // Wait 500 ms, more then 120 ms
}
// --- SOFT Ware Reset
tftSendCommand(0x01) ; // SOFTWARE RESET
delay(50);
// --- Initial Comands
tftSendCommand(0x28) ; // Display OFF
delay(500);
tftSendCommand(0x11) ; // Sleep Out
delay(500);
tftSendCommand1(0x3A,0x05) ; // 16Bit Pixel Mode
delay(10);
tftSendCommand1(0x36,B00000000) ; // MX MY MV ML RGB MH x x:縦向き1
tftSendCommand2(0xB6,0x15,0x02) ; // フレームレート設定
tftSendCommand(0x13) ; // NomalDisplayMode
tftSendCommand(0x21) ; // Display Inversion Off
tftSendCommand(0x29) ; // Display ON
tftSendCommand1(0x36,B11000000) ; // MX MY MV ML RGB MH x x:縦向き2
dispStartLine(80);
spi_lock(LOCK_NONE);
}
void show() {
spi_lock(LOCK_ST7789);
drawRGBBitmap(0,0,color565,240,240);
spi_lock(LOCK_NONE);
}
void capture() {
// put your main code here, to run repeatedly:
uint8_t temp = 0xff, temp_last = 0;
bool is_header = false;
uint16_t *imgpt;
temp = 0xff;
myCAM.flush_fifo();
myCAM.clear_fifo_flag();
myCAM.start_capture();
spi_lock(LOCK_ARDUCAM);
while(1) {
if (myCAM.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK)) {
delay(50);
uint8_t temp, temp_last;
uint32_t length = 0;
length = myCAM.read_fifo_length();
if (length >= MAX_FIFO_SIZE ) {
myCAM.clear_fifo_flag();
spi_lock(LOCK_NONE);
return;
}
if (length == 0 ) { //0 kb
myCAM.clear_fifo_flag();
spi_lock(LOCK_NONE);
return;
}
myCAM.CS_LOW();
myCAM.set_fifo_burst();//Set fifo burst mode
SPI.transfer(0x00);
char VH, VL;
int y = 0, x = 0;
int col = 0;
imgpt = color565;
for (y=0; y<240; y++) {
imgpt += 240;
for (x=0; x<320; x++) {
VH = SPI.transfer(0x00);
VL = SPI.transfer(0x00);
if ((x>39)&&(x<280)) {
imgpt--;
*imgpt = VH|(VL<<8);
}
delayMicroseconds(12);
}
imgpt += 240;
}
myCAM.CS_HIGH();
//Clear the capture done flag
myCAM.clear_fifo_flag();
break;
} else {
delay(100);
}
}
spi_lock(LOCK_NONE);
}
void init_ArduCam() {
uint8_t vid, pid;
uint8_t temp;
Serial1.println(F("ACK CMD ArduCAM Start! END"));
//Reset the CPLD
myCAM.write_reg(0x07, 0x80);
delay(100);
myCAM.write_reg(0x07, 0x00);
delay(100);
while(1){
//Check if the ArduCAM SPI bus is OK
myCAM.write_reg(ARDUCHIP_TEST1, 0x55);
temp = myCAM.read_reg(ARDUCHIP_TEST1);
if (temp != 0x55){
Serial1.println(F("ACK CMD SPI interface Error! END"));
delay(1000);continue;
}else{
Serial1.println(F("ACK CMD SPI interface OK. END"));break;
}
}
while(1){
//Check if the camera module type is OV2640
myCAM.wrSensorReg8_8(0xff, 0x01);
myCAM.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid);
myCAM.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid);
if ((vid != 0x26 ) && (( pid != 0x41 ) || ( pid != 0x42 ))){
Serial1.println(F("ACK CMD Can't find OV2640 module! END"));
delay(1000);continue;
} else {
Serial1.println(F("ACK CMD OV2640 detected. END"));break;
}
}
myCAM.OV2640_set_JPEG_size(OV2640_320x240); delay(1000);
myCAM.set_format(BMP);
myCAM.InitCAM();
myCAM.wrSensorReg16_8(0x3818, 0x81);
myCAM.wrSensorReg16_8(0x3621, 0xA7);
}
void setup() {
color565 = (uint16_t*)malloc(240*240*2);
if (!color565) {
while(1);
}
Wire.begin();
// set the CS as an output:
pinMode(SPI_ARDUCAM_CS,OUTPUT);
pinMode(SPI_TFT_CS, OUTPUT);
pinMode(SPI_DC, OUTPUT);
digitalWrite(SPI_ARDUCAM_CS, HIGH);
digitalWrite(SPI_TFT_CS, HIGH);
SPI.begin();
init_ArduCam();
init_tft();
}
void loop() {
capture();
show();
}
■自作デジカメの特性を把握する
デジカメ初号機と同様、弐号機も処理速度の観点から、動いている被写体の撮影は想定していません。
ですが、自作デジカメの特性を理解しておくことは重要です。
試しにストリーミング撮影において、ゆっくりと動いているものを撮影するとどうなるか確認してみました。
ArduCamの特性評価のためにのみ購入した「おだてブタ ポチッとな! クリップケース」です。
「豚もおだてりゃ木に登る」とは、日本語のことわざの一つ。普段は無能な者でも、おだててその気にさせると期待以上の成果を出すことがあるという譬え。不可能なことの譬えとして「豚の木登り」ということわざもある。 ウィキペディア


撮影した画像の表示にはタイムラグがあり、撮影間隔も1秒以上ありますが、ゆっくり動くものであれば撮影画像がぶれることはありませんでした。
■参考文献
・Arducam Chip
・SDカード速度比較
・ESP32 の SPI_MODE が修正
|
Raspberry Pi(ラズベリー パイ)は、ARMプロセッサを搭載したシングルボードコンピュータ。イギリスのラズベリーパイ財団によって開発されている。
たいていのことは100日あれば、うまくいく。長田英知著
「時間がなくて、なかなか自分のやりたいことができない」
「一念発起して何かを始めても、いつも三日坊主で終わってしまう」
「色んなことを先延ばしにしたまま、時間だけが過ぎていく」
そこで本書では、そんな著者が独自に開発した、
まったく新しい目標達成メソッド「100日デザイン」について、
その知識と技術を、余すところなくご紹介します。
まんがで納得ナポレオン・ヒル 思考は現実化する
OLとして雑務をこなす日々に飽き足らず、科学者だった父が残した薬品を商品化すべく、起業を決意した内山麻由(27)。彼女はセミナーで知り合った謎の女性からサポートを得ながら、彼女と二人三脚でナポレオン・ヒルの成功哲学を実践し、さまざまな問題を乗り越えていく。
ヒル博士の<ゴールデンルール>に従い、仕事に、恋に全力疾走する彼女の、成功への物語。
今日は人生最悪で最高の日 1秒で世界を変えるたったひとつの方法 ひすいこたろう著
偉人の伝記を読むと、最悪な日は、不幸な日ではなく、新しい自分が始まる日であることがわかります。最悪な出来事は、自分の人生が、想像を超えて面白くなる兆しなのです。偉人伝を読むことで、このときの不幸があったおかげで、未来にこういう幸せがくるのかと、人生を俯瞰する視線が立ち上がるのです。
ご飯は私を裏切らない heisoku著
辛い現実から目を背けて食べるご飯は、いつも美味しく幸せを届けてくれる。
29歳、中卒、恋人いない歴イコール年齢。バイト以外の職歴もなく、短期バイトを転々とする日々。ぐるぐると思索に耽るけど、ご飯を食べると幸せになれる。奇才の新鋭・heisokuが贈るリアル労働グルメ物語!
【最新版Gemini 3に対応!】できるGemini (できるシリーズ)
Geminiを「最強の知的生産パートナー」として使いこなすための、実践的なノウハウを凝縮した一冊です。
基本的な操作方法から、具体的なビジネスシーンでの活用、日々の業務を自動化するGoogle Workspaceとの連携、さらには自分だけのオリジナルAIを作成する方法まで余すところなく解説します。
Rustプログラミング完全ガイド 他言語との比較で違いが分かる!
Rustの各手法や考え方を幅広く解説!
500以上のサンプルを掲載。実行結果も確認。
全24章の包括的なチュートリアル。
ポチらせる文章術
販売サイト・ネット広告・メルマガ・ブログ・ホームページ・SNS…
全WEB媒体で効果バツグン!
カリスマコピーライターが教える「見てもらう」「買ってもらう」「共感してもらう」すべてに効くネット文章術
小型で便利な Type-C アダプター USB C オス - USB3.1 オスアダプター
Type-C端子のマイコンボードをこのアダプタを介して直接Raspberry Piに挿すことができます。ケーブルなしで便利なツールです。
Divoom Ditoo Pro ワイヤレススピーカー
15W高音質重低音/青軸キーボード/Bluetooth5.3/ピクセルアート 専用アプリ/USB接続/microSDカード
電源供給USBケーブル スリム 【5本セット】
USB電源ケーブル 5V DC電源供給ケーブル スリム 【5本セット】 電源供給 バッテリー 修理 自作 DIY 電子工作 (100cm)
|