-
-
Save Yuikawa-Akira/7e2eaef2d79a73e506a7a853aa58a739 to your computer and use it in GitHub Desktop.
| #include <esp_camera.h> | |
| #include <FastLED.h> | |
| #include <SPI.h> | |
| #include <SD.h> | |
| #include <M5Unified.h> | |
| #define KEY_PIN 1 | |
| #define LED_PIN 2 | |
| #define POWER_GPIO_NUM 18 | |
| CRGB LED[1]; | |
| camera_fb_t* fb; | |
| M5Canvas canvas0; | |
| // SDカード保存用 | |
| char filename[64]; | |
| int filecounter = 1; | |
| uint8_t graydata[240 * 176]; // HQVGAが今のところ最適 | |
| // 最大8色のカラーパレット | |
| uint32_t ColorPalettes[8][8] = { | |
| { // パレット0 slso8 | |
| 0x0D2B45, 0x203C56, 0x544E68, 0x8D697A, 0xD08159, 0xFFAA5E, 0xFFD4A3, 0xFFECD6 }, | |
| { // パレット1 都市伝説解体センター風 | |
| 0x000000, 0x000B22, 0x112B43, 0x437290, 0x437290, 0xE0D8D1, 0xE0D8D1, 0xFFFFFF }, | |
| { // パレット2 ファミレスを享受せよ風 | |
| 0x010101, 0x33669F, 0x33669F, 0x33669F, 0x498DB7, 0x498DB7, 0xFBE379, 0xFBE379 }, | |
| { // パレット3 gothic-bit | |
| 0x0E0E12, 0x1A1A24, 0x333346, 0x535373, 0x8080A4, 0xA6A6BF, 0xC1C1D2, 0xE6E6EC }, | |
| { // パレット4 noire-truth | |
| 0x1E1C32, 0x1E1C32, 0x1E1C32, 0x1E1C32, 0xC6BAAC, 0xC6BAAC, 0xC6BAAC, 0xC6BAAC }, | |
| { // パレット5 2BIT DEMIBOY | |
| 0x252525, 0x252525, 0x4B564D, 0x4B564D, 0x9AA57C, 0x9AA57C, 0xE0E9C4, 0xE0E9C4 }, | |
| { // パレット6 deep-maze | |
| 0x001D2A, 0x085562, 0x009A98, 0x00BE91, 0x38D88E, 0x9AF089, 0xF2FF66, 0xF2FF66 }, | |
| { // パレット7 night-rain | |
| 0x000000, 0x012036, 0x3A7BAA, 0x7D8FAE, 0xA1B4C1, 0xF0B9B9, 0xFFD159, 0xFFFFFF }, | |
| }; | |
| int currentPalettelndex = 0; // 現在のパレットのインデックス | |
| int maxPalettelndex = 8; // パレット総数 | |
| // TailBATを使用しているとき、消費電流が45mA以下だと電源がシャットダウンしてしまう対策 | |
| uint32_t LED_ON_DURATION = 120000; // LED 点灯時間 (ミリ秒) | |
| uint32_t keyOnTime = 0; // キースイッチを操作した時間 | |
| int dither = 0; // 0 = ディザ未使用 1 = ディザ使用 | |
| int levels = 8; // ディザ階調数 2以上 | |
| camera_config_t camera_config = { | |
| .pin_pwdn = -1, | |
| .pin_reset = -1, | |
| .pin_xclk = 21, | |
| .pin_sscb_sda = 12, | |
| .pin_sscb_scl = 9, | |
| .pin_d7 = 13, | |
| .pin_d6 = 11, | |
| .pin_d5 = 17, | |
| .pin_d4 = 4, | |
| .pin_d3 = 48, | |
| .pin_d2 = 46, | |
| .pin_d1 = 42, | |
| .pin_d0 = 3, | |
| .pin_vsync = 10, | |
| .pin_href = 14, | |
| .pin_pclk = 40, | |
| .xclk_freq_hz = 20000000, | |
| .ledc_timer = LEDC_TIMER_0, | |
| .ledc_channel = LEDC_CHANNEL_0, | |
| .pixel_format = PIXFORMAT_RGB565, | |
| .frame_size = FRAMESIZE_HQVGA, | |
| // FRAMESIZE_96X96, // 96x96 | |
| // FRAMESIZE_QQVGA, // 160x120 | |
| // FRAMESIZE_QCIF, // 176x144 | |
| // FRAMESIZE_HQVGA, // 240x176 | |
| // FRAMESIZE_240X240, // 240x240 | |
| // FRAMESIZE_QVGA, // 320x240 | |
| .jpeg_quality = 0, | |
| .fb_count = 2, | |
| .fb_location = CAMERA_FB_IN_PSRAM, | |
| .grab_mode = CAMERA_GRAB_LATEST, | |
| .sccb_i2c_port = 0, | |
| }; | |
| bool loadPaletteFromSD(int paletteIndex) { | |
| if (paletteIndex < 0 || paletteIndex > 7) { | |
| return false; | |
| } | |
| // ファイル名を生成 (例: /ColorPalette0.txt) | |
| String filename = "/ColorPalette" + String(paletteIndex) + ".txt"; | |
| // ファイルが存在するか確認 | |
| if (!SD.exists(filename)) { | |
| return false; // ファイルが存在しない場合はデフォルトを使うのでfalseを返す | |
| } | |
| // ファイルを開く | |
| File file = SD.open(filename, FILE_READ); | |
| if (!file) { | |
| return false; // ファイルオープン失敗 | |
| } | |
| // ファイルから8つのカラーコードを読み込む | |
| int colorCount = 0; | |
| while (file.available() && colorCount < 8) { | |
| String line = file.readStringUntil('\n'); // 1行読み込む | |
| line.trim(); // 前後の空白や改行文字を削除 | |
| if (line.length() > 0) { | |
| // strtoul(const char *str, char **endptr, int base) | |
| // base=0 で 0x (16進), 0 (8進), それ以外 (10進) を自動判別 | |
| uint32_t colorValue = strtoul(line.c_str(), NULL, 0); | |
| // エラーチェック (strtoulはエラー時に0を返すことがあるが、0x000000も有効な色なので完全ではない) | |
| // ここでは単純に読み込んだ値を格納する | |
| ColorPalettes[paletteIndex][colorCount] = colorValue; | |
| colorCount++; | |
| } | |
| } | |
| file.close(); // ファイルを閉じる | |
| // 8色読み込めたか確認 | |
| if (colorCount == 8) { | |
| return true; // 成功 | |
| } else { | |
| // もし8色以下の場合は読み込めた分だけ反映して残りはデフォルトを使用する | |
| return false; // 読み込み失敗(色が足りない) | |
| } | |
| } | |
| bool CameraBegin() { | |
| esp_err_t err = esp_camera_init(&camera_config); | |
| if (err != ESP_OK) { | |
| return false; | |
| } | |
| //カメラ追加設定 | |
| sensor_t* s = esp_camera_sensor_get(); | |
| s->set_vflip(s, 0); //上下反転 0無効 1有効 | |
| s->set_hmirror(s, 0); //左右反転 0無効 1有効 | |
| // s->set_colorbar(s, 1); //カラーバー 0無効 1有効 | |
| // s->set_brightness(s, 1); // up the brightness just a bit | |
| // s->set_saturation(s, 0); // lower the saturation | |
| return true; | |
| } | |
| bool CameraGet() { | |
| fb = esp_camera_fb_get(); | |
| if (!fb) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool CameraFree() { | |
| if (fb) { | |
| esp_camera_fb_return(fb); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void saveToSD_OriginalBMP() { | |
| sprintf(filename, "/%010d_%04d_Original.bmp", keyOnTime, filecounter); | |
| File file = SD.open(filename, "w"); | |
| if (file) { | |
| uint8_t* out_bmp = NULL; | |
| size_t out_bmp_len = 0; | |
| frame2bmp(fb, &out_bmp, &out_bmp_len); | |
| file.write(out_bmp, out_bmp_len); | |
| file.close(); | |
| free(out_bmp); | |
| } else { | |
| LED[0] = CRGB::Red; | |
| FastLED.show(); //Error! | |
| } | |
| } | |
| void saveToSD_ConvertBMP() { | |
| sprintf(filename, "/%010d_%04d_palette%01d.bmp", keyOnTime, filecounter, currentPalettelndex); | |
| File file = SD.open(filename, "w"); | |
| if (file) { | |
| int width = fb->width; | |
| int height = fb->height; | |
| int rowSize = (3 * width + 3) & ~3; | |
| lgfx::bitmap_header_t bmpheader; | |
| bmpheader.bfType = 0x4D42; | |
| bmpheader.bfSize = rowSize * height + sizeof(bmpheader); | |
| bmpheader.bfOffBits = sizeof(bmpheader); | |
| bmpheader.biSize = 40; | |
| bmpheader.biWidth = width; | |
| bmpheader.biHeight = height; | |
| bmpheader.biPlanes = 1; | |
| bmpheader.biBitCount = 24; | |
| bmpheader.biCompression = 0; | |
| bmpheader.biSizeImage = 0; //以下、MacOS向けに追加 | |
| bmpheader.biXPelsPerMeter = 2835; | |
| bmpheader.biYPelsPerMeter = 2835; | |
| bmpheader.biClrUsed = 0; | |
| bmpheader.biClrImportant = 0; | |
| file.write((std::uint8_t*)&bmpheader, sizeof(bmpheader)); | |
| std::uint8_t buffer[rowSize]; | |
| memset(&buffer[rowSize - 4], 0, 4); | |
| for (int y = height - 1; y >= 0; y--) { | |
| for (int x = 0; x < width; x++) { | |
| //グレイデータを読み出す | |
| int i_gray = y * width + x; | |
| uint8_t gray = graydata[i_gray]; | |
| //カラーパレットから色を取得 | |
| uint32_t newColor = ColorPalettes[currentPalettelndex][gray]; | |
| uint8_t r = (newColor >> 16) & 0xFF; | |
| uint8_t g = (newColor >> 8) & 0xFF; | |
| uint8_t b = newColor & 0xFF; | |
| //バッファに書き込み BGRの順になる | |
| int i_buffer = x * 3; | |
| buffer[i_buffer] = b; | |
| buffer[i_buffer + 1] = g; | |
| buffer[i_buffer + 2] = r; | |
| } | |
| file.write(buffer, rowSize); | |
| } | |
| file.close(); | |
| } else { | |
| LED[0] = CRGB::Red; | |
| FastLED.show(); //Error! | |
| } | |
| } | |
| void saveGraylevel_fromFb() { | |
| uint8_t* fb_data = fb->buf; | |
| int width = fb->width; | |
| int height = fb->height; | |
| int i = 0; | |
| for (int y = 0; y < height; y++) { | |
| for (int x = 0; x < (width * 2); x = x + 2) { | |
| // 各ピクセルの色を取得 | |
| uint32_t rgb565Color = (fb_data[y * width * 2 + x] << 8) | fb_data[y * width * 2 + x + 1]; | |
| // RGB565からRGB888へ変換 | |
| uint32_t rgb888Color = canvas0.color16to24(rgb565Color); | |
| uint8_t r = (rgb888Color >> 16) & 0xFF; | |
| uint8_t g = (rgb888Color >> 8) & 0xFF; | |
| uint8_t b = rgb888Color & 0xFF; | |
| // 輝度の計算 BT.709の係数を使用 | |
| uint16_t luminance = (uint16_t)(0.2126 * r + 0.7152 * g + 0.0722 * b); | |
| // 輝度を8階調のグレースケールに変換 | |
| uint8_t grayLevel = luminance / 32; // 256/32 = 8 | |
| // 輝度情報を保存 | |
| graydata[i] = grayLevel; | |
| i++; | |
| } | |
| } | |
| } | |
| void saveGraylevel_fromCanvas(M5Canvas& srcSprite) { | |
| int width = srcSprite.width(); | |
| int height = srcSprite.height(); | |
| int i = 0; | |
| for (int y = 0; y < height; y++) { | |
| for (int x = 0; x < width; x++) { | |
| // 各ピクセルの色を取得 | |
| uint32_t rgb565Color = srcSprite.readPixel(x, y); | |
| // RGB565からRGB888へ変換 | |
| uint32_t rgb888Color = srcSprite.color16to24(rgb565Color); | |
| uint8_t r = (rgb888Color >> 16) & 0xFF; | |
| uint8_t g = (rgb888Color >> 8) & 0xFF; | |
| uint8_t b = rgb888Color & 0xFF; | |
| // 輝度の計算 BT.709の係数を使用 | |
| uint16_t luminance = (uint16_t)(0.2126 * r + 0.7152 * g + 0.0722 * b); | |
| // 輝度を16階調のグレースケールに変換 | |
| uint8_t grayLevel = luminance / 32; // 256/32 = 8 | |
| // 輝度情報を保存 | |
| graydata[i] = grayLevel; | |
| i++; | |
| } | |
| } | |
| } | |
| void applyColorBayerDither4x4(M5Canvas& srcSprite, M5Canvas& dstSprite, int levelsPerChannel) { | |
| // Bayerマトリックス | |
| static const uint8_t bayer4x4[4][4] = { | |
| { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, { 3, 11, 1, 9 }, { 15, 7, 13, 5 } | |
| }; | |
| static const float bayerDivisor = 16.0f; | |
| int width = dstSprite.width(); | |
| int height = dstSprite.height(); | |
| float step = 255.0f / (float)(levelsPerChannel - 1); | |
| for (int y = 0; y < height; ++y) { | |
| for (int x = 0; x < width; ++x) { | |
| if (x >= srcSprite.width() || y >= srcSprite.height()) { | |
| dstSprite.drawPixel(x, y, TFT_BLACK); | |
| continue; | |
| } | |
| // 元画像のピクセル色を取得 | |
| uint32_t rgb565Color = srcSprite.readPixel(x, y); | |
| uint32_t originalColorValue = srcSprite.color16to24(rgb565Color); | |
| uint8_t r_src = (originalColorValue >> 16) & 0xFF; | |
| uint8_t g_src = (originalColorValue >> 8) & 0xFF; | |
| uint8_t b_src = originalColorValue & 0xFF; | |
| // 各チャンネルに対してディザリングを適用 | |
| uint8_t r_dst, g_dst, b_dst; | |
| uint8_t channels_src[3] = { r_src, g_src, b_src }; | |
| uint8_t channels_dst[3]; | |
| float bayerThreshold = (float)bayer4x4[y % 4][x % 4] / bayerDivisor; | |
| for (int ch = 0; ch < 3; ++ch) { | |
| uint8_t val_src = channels_src[ch]; | |
| int level_index = floor((float)val_src / step); | |
| if (level_index >= levelsPerChannel - 1) { level_index = levelsPerChannel - 2; } | |
| float level_low = (float)level_index * step; | |
| float error = (float)val_src - level_low; | |
| float normalized_error = (step > 0) ? (error / step) : 0.0f; | |
| if (normalized_error < 0.0f) normalized_error = 0.0f; | |
| if (normalized_error > 1.0f) normalized_error = 1.0f; | |
| uint8_t val_dst; | |
| if (normalized_error >= bayerThreshold) { | |
| val_dst = (uint8_t)round(((float)level_index + 1.0f) * step); | |
| } else { | |
| val_dst = (uint8_t)round(level_low); | |
| } | |
| channels_dst[ch] = std::max(0, std::min(255, (int)val_dst)); | |
| } | |
| r_dst = channels_dst[0]; | |
| g_dst = channels_dst[1]; | |
| b_dst = channels_dst[2]; | |
| // 新しいRGB値で出力先スプライトに描画 | |
| uint16_t ditheredColor = dstSprite.color565(r_dst, g_dst, b_dst); | |
| dstSprite.drawPixel(x, y, ditheredColor); | |
| } | |
| // delay(0); | |
| } | |
| } | |
| void setup() { | |
| M5.begin(); | |
| pinMode(POWER_GPIO_NUM, OUTPUT); | |
| digitalWrite(POWER_GPIO_NUM, LOW); | |
| delay(500); | |
| pinMode(KEY_PIN, INPUT_PULLUP); | |
| FastLED.addLeds<SK6812, LED_PIN, GRB>(LED, 1); | |
| LED[0] = CRGB::Red; | |
| FastLED.setBrightness(200); | |
| // 一度SDカードをマウントして確認 | |
| SPI.begin(7, 8, 6, -1); | |
| if (!SD.begin(15, SPI, 10000000)) { | |
| FastLED.show(); // エラー | |
| delay(500); | |
| return; | |
| } else { | |
| // パレット0から7までループ | |
| for (int i = 0; i < 8; i++) { | |
| if (loadPaletteFromSD(i)) { | |
| //M5.Display.printf("Palette %d loaded from SD.\n", i); | |
| } else { | |
| //M5.Display.printf("Palette %d use default.\n", i); | |
| } | |
| delay(100); | |
| } | |
| } | |
| SD.end(); // 一旦ENDしておく | |
| if (psramFound()) { | |
| camera_config.pixel_format = PIXFORMAT_RGB565; | |
| camera_config.fb_location = CAMERA_FB_IN_PSRAM; | |
| camera_config.fb_count = 2; | |
| } else { | |
| FastLED.show(); // エラー | |
| delay(500); | |
| } | |
| if (!CameraBegin()) { | |
| FastLED.show(); // エラー | |
| delay(1000); | |
| ESP.restart(); | |
| } | |
| delay(500); | |
| // ボタン押しながら起動でディザモード | |
| if (!digitalRead(KEY_PIN)) { | |
| dither = 1; | |
| canvas0.createSprite(240, 176); | |
| LED[0] = CRGB::Blue; | |
| FastLED.setBrightness(200); | |
| FastLED.show(); | |
| } | |
| delay(500); | |
| LED[0] = CRGB::LimeGreen; | |
| FastLED.setBrightness(200); | |
| FastLED.show(); | |
| } | |
| void loop() { | |
| if (!digitalRead(KEY_PIN)) { | |
| keyOnTime = millis(); // 最後にkey操作した時間 | |
| LED[0] = CRGB::Orange; | |
| FastLED.setBrightness(20); | |
| FastLED.show(); | |
| CameraGet(); // 撮影 | |
| SD.end(); // 念のため一旦END | |
| delay(100); | |
| SD.begin(15, SPI, 10000000); | |
| saveToSD_OriginalBMP(); // 変換前の画像保存 | |
| if (dither == 0) { | |
| saveGraylevel_fromFb(); // 輝度情報の保存 | |
| } else { | |
| canvas0.pushImage(0, 0, 240, 176, (uint16_t*)fb->buf); // (x, y, w, h, *data) | |
| applyColorBayerDither4x4(canvas0, canvas0, levels); | |
| saveGraylevel_fromCanvas(canvas0); | |
| } | |
| for (int i = 0; i < maxPalettelndex; i++) { | |
| currentPalettelndex = i; | |
| FastLED.setBrightness(i * 20 + 40); // 処理が進むごとに明るくする | |
| FastLED.show(); | |
| saveToSD_ConvertBMP(); // 変換後の画像保存 | |
| } | |
| CameraFree(); // フレームバッファを解放 | |
| filecounter++; // 連番を更新 | |
| SD.end(); | |
| LED[0] = CRGB::LimeGreen; | |
| FastLED.setBrightness(200); | |
| FastLED.show(); | |
| } | |
| //一定時間操作していないとLEDをOFF 平均消費電流が45mAを下回るとTailBATが40秒後に自動OFFする | |
| if ((millis() - keyOnTime >= LED_ON_DURATION)) { | |
| LED[0] = CRGB::Black; | |
| FastLED.show(); | |
| } | |
| } |
| 0x002B59 | |
| 0x002B59 | |
| 0x005F8C | |
| 0x005F8C | |
| 0x00B9BE | |
| 0x00B9BE | |
| 0x9FF4E5 | |
| 0x9FF4E5 |
| /* | |
| ・ATOMS3R カメラキット | |
| https://www.switch-science.com/products/9916 | |
| https://shop.m5stack.com/products/atoms3r-camera-kit | |
| ・ATOMIC TFカードリーダー | |
| https://www.switch-science.com/products/9423 | |
| https://shop.m5stack.com/products/atomic-tf-card-reader | |
| ・M5Stack用メカニカルキーボタンユニット | |
| https://www.switch-science.com/products/8303 | |
| https://shop.m5stack.com/products/mechanical-key-button-unit | |
| ・ATOM TailBAT | |
| https://www.switch-science.com/products/6348 | |
| https://shop.m5stack.com/products/atom-tailbat?variant=32169047064666 | |
| ・LEGO テクニック 固定ピン 2本(Amazon 楽天などで入手可能) | |
| https://amzn.to/4iItP41 | |
| ・microSDカード 16GBまで | |
| https://amzn.to/4hnl7XS | |
| */ |
thanks,
i had outdated M5Unified and M5Gfx libraries
works now
nice work on the sketch
Arduinoで書き込みを行った結果
Compilation error: 'esp_spiram_get_size' was not declared in this scope
というエラーメッセージが出てしまいました。
ボードはM5AtomS3を認識しておりesp32 by Espressif Systemsは3.2.1をインストールしています。
プログラムの知識がなく、解決策がわからないので良ければお教えください
ボードの設定を上記のように行ったのですが、M5AtomS3を認識してしまっておりM5AtomS3Rとは認識してくれません
手動で一番上のボード設定をM5AtomS3Rに変えてコンパイルしてみてください
ボード設定を変えて、ボードをM5AtomS3Rに変更しても
C:\Users\e6a250180e\Downloads\kamera\kamera.ino: In function 'void setup()':
C:\Users\e6a250180e\Downloads\kamera\kamera.ino:299:25: error: 'esp_spiram_get_size' was not declared in this scope
299 | size_t psram_size = esp_spiram_get_size() / 1048576;
| ^~~~~~~~~~~~~~~~~~~
exit status 1
Compilation error: 'esp_spiram_get_size' was not declared in this scope
というエラーメッセージが出てしまいます
size_t psram_size = esp_spiram_get_size() / 1048576;の行をコメントアウトしてみてください
↓
//size_t psram_size = esp_spiram_get_size() / 1048576;
できました!!
本当にありがとうございます!!!!!
書き込む際に以下のようなエラーが出てしまいます、何が原因でしょうか。
A fatal error occurred: Failed to connect to ESP32-S3: No serial data received.
For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
Failed uploading: uploading error: exit status 2
内蔵の緑色のLEDが点灯するまで側面のリセットボタンを長押し(約2秒)してから離すとダウンロードモードになり、書き込み待機状態になるので試してみてください。
それでもダメならばCOMポートの番号が違うかもしれないので別のポート番号を選んでみてください。
ダウンロードモードにして、シリアルポートをjdev/cu.usbmodem21101にすることで解決しました!ありがとうございます!
たくさん写真撮ってきます!


with CDC, DIO and QSPI. are these right?
using the AtomS3R-M12