三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」ー人感センサーで撮影するサーマルカメラ完成ー [SPRESENSE]
三月はすごい忙しかったことに加えて、長年連れ添ってきたオンボロ3Dプリンターがついに壊れてしまい、作業を中断せざる得なかったということがあります。(新調した3Dプリンタのレビューは後日やります!)
で、かなり遅れてしまったのですがついに完成しました。人勧センサーで撮影する低消費電力サーマルカメラです。なんとこれだけ色々ついていて待ち受け時の消費電流は10uAもありません。
ご覧になってわかるように、SPRESENSE本体の筐体は、一月の電子工作「タイムラプスカメラ」で制作した”プレッセ君”の流用です。では、制作過程をご紹介しましょう。
■ 人感センサー付きサーマルカメラのカメラ部のメカ
カメラ部は、SPRESENSEカメラとサーマルセンサアレイAMG8833、人感センサーをまとめて一つのパッケージにすることしました。
カメラ部の3Dデザインはかなり困難を極めました。AMG8833ボードの外形図、人感センサーボードの外形図がないことに加え、SPRESENSEカメラとAMG8833の光軸をなるべく近づけるために目合わせで調整しなければならず、3回作り直すことになりました。
面倒なことに、AMG8833用の電源とI2Cライン、人感センサー用の電源と割り込み用GPIOの合計7本の線が必要です。電源とGNDを共通にするために、間にボードを挟むことにしました。
メインボードからの電源は、3.3V電源でなく EXT_VDD(4.0V)を使っています(3.3VはSDカードが使用)。AMG8833の消費電力は10mA程度、人感センサーは10uA程度なので、電圧を下げるために間に100?を挟んでおきました。
このボードを格納するためのランドセルを作りカメラ部に接着剤でつけて背負わせることにしました。実は、このためのケーブルを作るのが一番面倒だった…。
■ 人感センサー付きサーマルカメラのSPRESENSE部
SPRESENSE本体は、一月に作ったタイムラプスカメラの筐体を流用しますが、ケーブルを通すための穴が必要です。穴をあけて新しい3Dプリンタで出力しました。
カメラ部をホールドするためのアームが必要なのですが、カメラ部のランドセルが干渉するので曲げることにしました。ロボットのようでちょっとかわいいです。
カメラ部と SPRESENSEメインボードのケーブルは次のように結線されています。
■ SPRESENSEカメラの出力をBMPで保存する
今までSPRESENSEカメラはJPEGの出力を使っていたのでそのまま保存できたのですが、今回はAMG8833のサーマル画像を重畳しなければならないので、ビットマップでそのまま保存したい。ということで、16bit の BMPで保存するようにしました。
■ 人感センサーで動作するサーマルカメラを実現する
全ての材料が揃ったのでコーディングします。プログラムは少し長いので、GitHubにあげておきました。興味がある人は見てください。
さて実際に動かして撮れ高を見てみましょう。
これが撮影した画像です。320x240なので少し小さいです。(フォーマットがBMPではないのは、SSブログが受け付けてくれないためです…)
少し画像とサーマルイメージの位置関係がズレてしまっていますが、目合わせであわせこんだのでこんなものでしょう。
■ 作ってみた感想
もう少し小さく作れるかなと思ったのですが、思いのほか大きくなってしまいました。そのせいかタイムラプスカメラを作った時のような興奮がなかったのは残念です。でも人感センサーが思いのほか性能がよかったことと(きちんと調整できればですが)、16ビットのままBMPファイルで保存できるようになったのは大きな収穫です。
庭とかに放置して何がとれるか試してみようかな。
さて、4月は何を作ろう。ちょっと最近忙しいからあまり手間がかかるのはやめたいな…
(^^)
■ 関連記事
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」
https://makers-with-myson.blog.ss-blog.jp/2021-03-07
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」カメラ画像にサーマル画像を重畳してみた
https://makers-with-myson.blog.ss-blog.jp/2021-03-14
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーを試してみた!
https://makers-with-myson.blog.ss-blog.jp/2021-03-21
で、かなり遅れてしまったのですがついに完成しました。人勧センサーで撮影する低消費電力サーマルカメラです。なんとこれだけ色々ついていて待ち受け時の消費電流は10uAもありません。
ご覧になってわかるように、SPRESENSE本体の筐体は、一月の電子工作「タイムラプスカメラ」で制作した”プレッセ君”の流用です。では、制作過程をご紹介しましょう。
■ 人感センサー付きサーマルカメラのカメラ部のメカ
カメラ部は、SPRESENSEカメラとサーマルセンサアレイAMG8833、人感センサーをまとめて一つのパッケージにすることしました。
カメラ部の3Dデザインはかなり困難を極めました。AMG8833ボードの外形図、人感センサーボードの外形図がないことに加え、SPRESENSEカメラとAMG8833の光軸をなるべく近づけるために目合わせで調整しなければならず、3回作り直すことになりました。
面倒なことに、AMG8833用の電源とI2Cライン、人感センサー用の電源と割り込み用GPIOの合計7本の線が必要です。電源とGNDを共通にするために、間にボードを挟むことにしました。
メインボードからの電源は、3.3V電源でなく EXT_VDD(4.0V)を使っています(3.3VはSDカードが使用)。AMG8833の消費電力は10mA程度、人感センサーは10uA程度なので、電圧を下げるために間に100?を挟んでおきました。
このボードを格納するためのランドセルを作りカメラ部に接着剤でつけて背負わせることにしました。実は、このためのケーブルを作るのが一番面倒だった…。
■ 人感センサー付きサーマルカメラのSPRESENSE部
SPRESENSE本体は、一月に作ったタイムラプスカメラの筐体を流用しますが、ケーブルを通すための穴が必要です。穴をあけて新しい3Dプリンタで出力しました。
カメラ部をホールドするためのアームが必要なのですが、カメラ部のランドセルが干渉するので曲げることにしました。ロボットのようでちょっとかわいいです。
カメラ部と SPRESENSEメインボードのケーブルは次のように結線されています。
■ SPRESENSEカメラの出力をBMPで保存する
今までSPRESENSEカメラはJPEGの出力を使っていたのでそのまま保存できたのですが、今回はAMG8833のサーマル画像を重畳しなければならないので、ビットマップでそのまま保存したい。ということで、16bit の BMPで保存するようにしました。
#include <SPI.h> #include <SPISD.h> #include <Camera.h> SpiSDClass SD(SPI5); static int gCounter = 0; /* Bitmap definition of RGB565 */ #define IMG_WIDTH (320) #define IMG_HEIGHT (240) #define BITS_PER_PIXEL (16) #define BI_BITFIELD (3) #define IMG_SIZE (IMG_WIDTH*IMG_HEIGHT*(BITS_PER_PIXEL/8)) #define HEADER_SIZE (54) #define INFO_HEADER_SIZE (40) #define MASK_SIZE (12) /* Bitmap Header parameters */ uint16_t bfType = 0x4D42; /* "BM" */ uint32_t bfSize = IMG_SIZE + HEADER_SIZE + MASK_SIZE; /* image size + 54 + 12*/ uint16_t bfReserved1 = 0; uint16_t bfReserved2 = 0; uint32_t bfOffBits = HEADER_SIZE + MASK_SIZE; uint32_t biSize = INFO_HEADER_SIZE; uint32_t biWidth = IMG_WIDTH; uint32_t biHeight = IMG_HEIGHT; uint16_t biPlanes = 1; uint16_t biBitCount = BITS_PER_PIXEL; uint32_t biCompression = BI_BITFIELD; uint32_t biSizeImage = IMG_SIZE; uint32_t biXPelsPerMeter = 4724; /* dummy */ uint32_t biYPelsPerMeter = 4724; /* dummy */ uint32_t biClrUsed = 0; uint32_t biClrImportant = 0; uint32_t biRmask = 0x0000f800; /* 16bit mask */ uint32_t biGmask = 0x000007e0; /* 16bit mask */ uint32_t biBmask = 0x0000001f; /* 16bit mask */ uint8_t WORD[2]; uint8_t* swap16(uint16_t word16) { WORD[0] = (uint8_t)(word16 & 0x00ff); WORD[1] = (uint8_t)((word16 & 0xff00) >> 8); return WORD; } uint8_t DWORD[4]; uint8_t* swap32(uint32_t dword32) { DWORD[0] = (uint8_t)(dword32 & 0x000000ff); DWORD[1] = (uint8_t)((dword32 & 0x0000ff00) >> 8); DWORD[2] = (uint8_t)((dword32 & 0x00ff0000) >> 16); DWORD[3] = (uint8_t)((dword32 & 0xff000000) >> 24); return DWORD; } uint8_t BMP_HEADER[HEADER_SIZE+MASK_SIZE]; void make_bmp_header() { uint8_t* word16; uint8_t* word32; int n = 0; word16 = swap16(bfType); /* "BM" */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word32 = swap32(bfSize); /* File Size */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word16 = swap16(bfReserved1); /* Reserved */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word16 = swap16(bfReserved2); /* Reserved */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word32 = swap32(bfOffBits); /* Offset to image data */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biSize); /* Bitmap info structure size (40) */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biWidth); /* Image width */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biHeight); /* Image height */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word16 = swap16(biPlanes); /* Image plane (almost 1) */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word16 = swap16(biBitCount); /* Pixel per bits */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word32 = swap32(biCompression); /* Complession type */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biSizeImage); /* Image size */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biXPelsPerMeter); /* Resolution (dummy) */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biYPelsPerMeter); /* Resolution (dummy) */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biClrUsed); /* Color used */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biClrImportant); /* Important Color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biRmask); /* Bitmask for red in case of 16bits color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biGmask); /* Bitmask for green in case of 16bits color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biBmask); /* Bitmask for blue in case of 16bits color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } if (n != HEADER_SIZE + MASK_SIZE) { Serial.println("HEADER_SIZE ERROR"); exit(1); } } void setup() { Serial.begin(115200); if (!SD.begin(SPI_FULL_SPEED)) { Serial.println("SD.begin() failed"); return; } make_bmp_header(); theCamera.begin(); CamErr err = theCamera.setStillPictureImageFormat( IMG_WIDTH ,IMG_HEIGHT ,CAM_IMAGE_PIX_FMT_YUV422); if (err != CAM_ERR_SUCCESS) { Serial.println("setStillPictureImageFormat Error: " + String(err)); return; } sleep(1); } void loop() { if (gCounter > 0) { Serial.println("Capturing End"); while(1){}; } Serial.println("Take Picture"); digitalWrite(LED0, HIGH); uint32_t start_time = millis(); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); char filename[16] = {0}; sprintf(filename, "PICT%03d.BMP", gCounter); if (SD.exists(filename)) { Serial.println("remove " + String(filename)); SD.remove(filename); } SpiFile myFile = SD.open(filename,FILE_WRITE); /* write bitmap header */ myFile.write(BMP_HEADER, (HEADER_SIZE+MASK_SIZE)*sizeof(uint8_t)); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); ++gCounter; } else { Serial.println("Capture Image Failed"); } digitalWrite(LED0, LOW); uint32_t duration = millis() - start_time; Serial.println("time (ms) = " + String(duration)); }
■ 人感センサーで動作するサーマルカメラを実現する
全ての材料が揃ったのでコーディングします。プログラムは少し長いので、GitHubにあげておきました。興味がある人は見てください。
YoshinoTaro/Spresense-HumanDetection-ThermalCamera Contribute to YoshinoTaro/Spresense-HumanDetection-ThermalCamera development by creating an account on GitHub. github.com |
さて実際に動かして撮れ高を見てみましょう。
これが撮影した画像です。320x240なので少し小さいです。(フォーマットがBMPではないのは、SSブログが受け付けてくれないためです…)
少し画像とサーマルイメージの位置関係がズレてしまっていますが、目合わせであわせこんだのでこんなものでしょう。
■ 作ってみた感想
もう少し小さく作れるかなと思ったのですが、思いのほか大きくなってしまいました。そのせいかタイムラプスカメラを作った時のような興奮がなかったのは残念です。でも人感センサーが思いのほか性能がよかったことと(きちんと調整できればですが)、16ビットのままBMPファイルで保存できるようになったのは大きな収穫です。
庭とかに放置して何がとれるか試してみようかな。
さて、4月は何を作ろう。ちょっと最近忙しいからあまり手間がかかるのはやめたいな…
(^^)
■ 関連記事
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」
https://makers-with-myson.blog.ss-blog.jp/2021-03-07
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」カメラ画像にサーマル画像を重畳してみた
https://makers-with-myson.blog.ss-blog.jp/2021-03-14
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーを試してみた!
https://makers-with-myson.blog.ss-blog.jp/2021-03-21
サーモグラフィー AMG8833搭載" title="Conta サーモグラフィー AMG8833搭載">
- 出版社/メーカー: スイッチサイエンス
- メディア:
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE カメラモジュール CXD5602PWBCAM1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
コメント 0