編集履歴一覧に戻る
t-kondoyのアイコン画像

t-kondoy が 2025年01月31日00時56分33秒 に編集

初版

タイトルの変更

+

Spresenseでデジタルドットカメラを考えた

タグの変更

+

SPRESENSE

+

spresense

+

ドット

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

製作品

ライセンスの変更

+

(MIT) The MIT License

本文の変更

+

2024年後半からデジタルアートに興味を持ち、自分の赴くままに制作を始めました。 それが結構楽しくて、Spresenseのコンテストでも何かアート系で作れないかと思い今回デジタルドットカメラを作る事にしました。

デジタルドットカメラはSONYのHDRカメラに映った画像をリアルタイムでドット絵のような画像にしてモニターに出力するカメラです。 このカメラの利点は撮影しても誰だか分からないのでプライバシーが常に守られている点。
シャッターボタンは無く、ランダムに撮影されるので、持っているだけで良い点 撮影した写真がそのまま作品になる点です。 使用部材 **Spresenseメインボード Spresense拡張ボード Spresense HDRカメラボード ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807** スライドスイッチ 1回路2接点 リチウムイオンバッテリー3.7V 600mA リチウム電池充電器基板 USB Type-C TP4056 5V 1A 3Dプリンタ用フィラメント MicroSDHC 32GB USBケーブル(MicroB) ※モニター試供品は太字にしています。 モデリング カメラの筐体はAutodesk Fusionで製作しました。 https://a360.co/3WHcgc1 プログラム ```arduino:Lチカの例 #define LED_PIN 13 void setup() { pinMode(LED_PIN, OUTPUT); } void loop() { digitalWrite(LED_PIN, HIGH); delay(1000); digitalWrite(LED_PIN, LOW); delay(1000); } #include <Camera.h> #include <SPI.h> #include "Adafruit_GFX.h" #include "Adafruit_ILI9341.h" // Spresense専用SDライブラリ #include <SDHCI.h> SDClass SD; // グローバルでSDオブジェクトを生成 // TFTディスプレイのピン定義 #define TFT_RST 8 #define TFT_DC 9 #define TFT_CS 10 // TFTディスプレイの初期化 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST); int blockSize = 4; // モザイクブロックサイズ unsigned long lastTimeFPS = 0; int frameCount = 0; // 10分(600000ms)ごとに1フレーム保存 unsigned long lastSaveTime = 0; const unsigned long SAVE_INTERVAL = 600000UL; // 10分(ミリ秒) // ファミコン風 25色パレット const uint16_t nesPalette[25] = { 0x0000, 0x0015, 0x006A, 0x00AA, 0x0800, 0x086A, 0x0CFF, 0x4F00, 0x5030, 0x7F00, 0x7F30, 0x7FFF, 0x9CCC, 0xAA00, 0xAA55, 0xBFFF, 0xCF00, 0xD555, 0xEBAA, 0xF555, 0xFFFF }; // ---------------------------------------------------------------------------- // SPI通信速度を最適化(25MHz) // ---------------------------------------------------------------------------- void setupSPI() { SPI.beginTransaction(SPISettings(25000000, MSBFIRST, SPI_MODE0)); // 25MHz tft.begin(); SPI.endTransaction(); } // ---------------------------------------------------------------------------- // FPSを測定 // ---------------------------------------------------------------------------- void measureFPS() { unsigned long now = millis(); frameCount++; if (now - lastTimeFPS >= 1000) { Serial.print("FPS: "); Serial.println(frameCount); frameCount = 0; lastTimeFPS = now; } } // ---------------------------------------------------------------------------- // ファミコン風の色へマッチング // ---------------------------------------------------------------------------- uint16_t matchPalette(uint16_t color) { // RGB565からR5,G6,B5を取り出す uint8_t r = (color >> 11) & 0x1F; uint8_t g = (color >> 5) & 0x3F; uint8_t b = color & 0x1F; uint16_t bestMatch = 0; uint32_t minDist = 0xFFFFFFFF; for (int i = 0; i < 25; i++) { uint8_t pr = (nesPalette[i] >> 11) & 0x1F; uint8_t pg = (nesPalette[i] >> 5) & 0x3F; uint8_t pb = nesPalette[i] & 0x1F; uint32_t dist = (pr - r)*(pr - r) + (pg - g)*(pg - g) + (pb - b)*(pb - b); if (dist < minDist) { minDist = dist; bestMatch = nesPalette[i]; } } return bestMatch; } // ---------------------------------------------------------------------------- // モザイク処理(ファミコン風パレット適用) // ---------------------------------------------------------------------------- void applyMosaic(CamImage &img, int blockSize) { if (!img.isAvailable()) return; uint16_t* buffer = (uint16_t*)img.getImgBuff(); int width = img.getWidth(); int height = img.getHeight(); for (int y = 0; y < height; y += blockSize) { for (int x = 0; x < width; x += blockSize) { uint32_t sumR = 0, sumG = 0, sumB = 0; int count = 0; // ブロック内の色を平均化 for (int j = 0; j < blockSize; j++) { for (int i = 0; i < blockSize; i++) { int index = (y + j) * width + (x + i); if (index >= width * height) continue; uint16_t color = buffer[index]; sumR += (color >> 11) & 0x1F; sumG += (color >> 5) & 0x3F; sumB += color & 0x1F; count++; } } // 平均色を算出 → ファミコン風パレットに近い色へ uint16_t avgColor = ((sumR / count) << 11) | ((sumG / count) << 5) | (sumB / count); uint16_t reducedColor = matchPalette(avgColor); // ブロック内をすべて同じ色にする for (int j = 0; j < blockSize; j++) { for (int i = 0; i < blockSize; i++) { int index = (y + j) * width + (x + i); if (index >= width * height) continue; buffer[index] = reducedColor; } } } } } // ---------------------------------------------------------------------------- // スキャンライン効果 // ---------------------------------------------------------------------------- void applyScanlineEffect(CamImage &img) { if (!img.isAvailable()) return; uint16_t* buffer = (uint16_t*)img.getImgBuff(); int width = img.getWidth(); int height = img.getHeight(); for (int y = 0; y < height; y++) { if (y % 2 == 0) { // 偶数行を暗くする for (int x = 0; x < width; x++) { int index = y * width + x; uint16_t color = buffer[index]; uint8_t r = (color >> 11) & 0x1F; uint8_t g = (color >> 5) & 0x3F; uint8_t b = color & 0x1F; // ざっくり4/5に暗く r = (r * 4) / 5; g = (g * 4) / 5; b = (b * 4) / 5; buffer[index] = (r << 11) | (g << 5) | b; } } } } // ---------------------------------------------------------------------------- // TFT描画(最適化版) // ---------------------------------------------------------------------------- void drawOptimized(CamImage img) { if (!img.isAvailable()) return; uint16_t* buffer = (uint16_t*)img.getImgBuff(); int width = img.getWidth(); int height = img.getHeight(); // 行単位で描画 for (int y = 0; y < height; y++) { tft.drawRGBBitmap(0, y, &buffer[y * width], width, 1); } } // ---------------------------------------------------------------------------- // RAWデータをそのままSDに保存する関数 // ---------------------------------------------------------------------------- bool saveRawFrame(const CamImage &img, const char* filename) { // 画像が利用可能&RGB565であることを確認 if (!img.isAvailable() || img.getPixFormat() != CAM_IMAGE_PIX_FMT_RGB565) { Serial.println("Image not available or not RGB565."); return false; } // SDカードに書き込み File f = SD.open(filename, FILE_WRITE); if (!f) { Serial.println("Failed to open file for writing."); return false; } uint8_t* buff = (uint8_t*)img.getImgBuff(); size_t size = img.getImgSize(); // width * height * 2 バイト f.write(buff, size); f.close(); Serial.println("Saved RAW frame to SD"); return true; } // ---------------------------------------------------------------------------- // カメラ画像のコールバック関数 // ---------------------------------------------------------------------------- void CamCB(CamImage img) { if (img.isAvailable()) { // FPS計測 measureFPS(); // RGB565に変換 img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); // モザイク効果(ファミコン風)+スキャンライン applyMosaic(img, blockSize); applyScanlineEffect(img); // 描画 drawOptimized(img); // --- 10分ごとに1枚だけRAW保存 --- unsigned long now = millis(); if (now - lastSaveTime >= SAVE_INTERVAL) { lastSaveTime = now; // RAW書き込み saveRawFrame(img, "/capture.raw"); // ファイルパスを"/capture.raw"に変更 } } } // ---------------------------------------------------------------------------- // setup() // ---------------------------------------------------------------------------- void setup() { Serial.begin(9600); // ボーレートを9600に設定 delay(2000); // シリアル接続安定のために少し待つ Serial.println("Starting main program..."); // SPI&TFT初期化 setupSPI(); tft.setRotation(3); // ★★★ SDマウント ★★★ // ここで SD.begin() を呼ぶ if (!SD.begin()) { Serial.println("Failed to initialize SD card."); } else { Serial.println("SD card initialized."); } // カメラ開始 CamErr err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { Serial.println("Error: Failed to initialize camera"); return; } // QVGAサイズ(RGB565)でスチル設定 → ストリーミングでも同等サイズ theCamera.setStillPictureImageFormat(CAM_IMGSIZE_QVGA_H, CAM_IMGSIZE_QVGA_V, CAM_IMAGE_PIX_FMT_RGB565); // ストリーミング開始(コールバック有効) theCamera.startStreaming(true, CamCB); // タイマー初期化 lastTimeFPS = millis(); lastSaveTime = millis(); // 起動直後を基準にする(初回は10分後に保存) } // ---------------------------------------------------------------------------- // loop() // ---------------------------------------------------------------------------- void loop() { // メインループは空でもOK(カメラコールバックが動き続ける) } ``` 個人的感想 今回初めてArduinoでの開発に限界を感じました。 と言うのもSpresenseはマルチコアですがArduinoではシングルコアでの開発しか出来ません。 Spresense SDK(C/C++)を使えばマルチコアでの開発が可能ですが、自分の実力不足が露呈しました。。。 本当はWiFiモジュールを取り付けているので、日時とその日の天気情報を使って、カラーパレットを生成したり、リアルタイム表示も若干ラグがあるので、もう1コア使えばかなり柔軟なプログラムが出来るだろうと製作してから感じました。 結局実力の6分の1しか使えないのでSpresense SDKで今後開発を継続したいと思います。 とは言え、世の中にないものを自分で考え形にするのはとても楽しいですね! 自分の欲しい感じになったのもとても良かったです! 「こんなの誰が欲しいと思うのか?」 2人くらいはいるんじゃないかなと思うんですよね。 その内の1人は自分ですが。 今回もとても良い経験が出来ました。 コンテストに参加した皆さんも本当にお疲れ様でした!