Skip to content

Instantly share code, notes, and snippets.

@Yuikawa-Akira
Last active April 10, 2025 19:44
Show Gist options
  • Save Yuikawa-Akira/1d96e692591a2d350d8fd0d02cd94f8a to your computer and use it in GitHub Desktop.
Save Yuikawa-Akira/1d96e692591a2d350d8fd0d02cd94f8a to your computer and use it in GitHub Desktop.
#include <esp_camera.h>
#include <SPI.h>
#include <SD.h>
//#include <M5UnitLCD.h>
#include <M5UnitGLASS2.h>
#include <M5Unified.h>
#include "Unit_Encoder.h"
#define POWER_GPIO_NUM 18
camera_fb_t* fb;
//M5UnitLCD display;
M5UnitGLASS2 display;
M5Canvas canvas0;
M5Canvas canvas1;
Unit_Encoder encoder; // Unit Encoderのインスタンス
bool buttonPressed = false; // ボタンの状態
signed short int newEncoderValue = 0; // エンコーダの値 新
signed short int lastEncoderValue = 0; // エンコーダの値 旧
int accumulatedChange = 0; // エンコーダ値の累積変化量
int mode = -1; // 撮影モード (-1から7)
const int modeMax = 7; // モードの最大値
char filename[64]; // SDカード保存ファイル名
char filesaveprogress[24]; // 保存中表示名
char currentmode[16]; // モード表示
int filecounter = 1; // ファイルカウンターは電源を入れるたびにリセットされる 極稀にファイル名が被るかも
uint8_t graydata[240 * 176]; // 輝度情報保存
int currentPalettelndex = 0; // 現在のパレットのインデックス
int maxPalettelndex = 8; // パレット総数
uint32_t LED_ON_DURATION = 100000; // LED 点灯時間 (ミリ秒)
uint32_t keyOnTime = 0; // キースイッチを操作した時間
float zoom = 0.5; // 拡大表示OLED用
// 最大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 },
};
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 CameraBegin() {
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
return false;
}
// カメラ追加設定
sensor_t* s = esp_camera_sensor_get();
// AtomS3R Cam のときはこちら
s->set_hmirror(s, 1); // 左右反転 0無効 1有効
s->set_vflip(s, 1); // 上下反転 0無効 1有効
// AtomS3R M12 のときはこちら
//s->set_lenc(s, 1); // レンズ補正? 効いてるか微妙
//s->set_hmirror(s, 1); // 左右反転 0無効 1有効
//s->set_vflip(s, 0); // 上下反転 0無効 1有効
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;
}
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; // 読み込み失敗(色が足りない)
}
}
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 {
error_lamp();
}
}
void saveToSD_DisplayBMP() {
sprintf(filename, "/%010d_%04d_Display.bmp", keyOnTime, filecounter);
File file = SD.open(filename, "w");
if (file) {
int width = display.width();
int height = display.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;
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--) {
display.readRect(0, y, width, 1, (lgfx::rgb888_t*)buffer);
file.write(buffer, rowSize);
}
file.close();
} else {
error_lamp();
}
}
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;
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 {
error_lamp();
}
}
void saveGraylevel_fb() {
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);
// 輝度を16階調のグレースケールに変換
uint8_t grayLevel = luminance / 32; // 256/32 = 8
// 輝度情報を保存
graydata[i] = grayLevel;
i++;
}
}
}
void convertColor_canvas() {
int width = display.width();
int height = display.height();
if (CameraGet()) {
canvas1.pushImage(0, -16, 240, 176, (uint16_t*)fb->buf); // (x, y, w, h, *data)
canvas1.pushSprite(&canvas0, 0, 0);
CameraFree(); // 取得したフレームを解放
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 各ピクセルの色を取得
uint32_t rgb565Color = canvas0.readPixel(x, y);
// 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);
// 輝度を16階調のグレースケールに変換
uint8_t grayLevel = luminance / 32; // 256/32 = 8
// カラーパレットから色を取得
uint32_t newColor = ColorPalettes[currentPalettelndex][grayLevel];
// 取得した色を書込
canvas0.drawPixel(x, y, canvas0.color24to16(newColor));
}
}
canvas0.pushSprite(&display, 0, 0);
}
void error_lamp() {
encoder.setLEDColor(0, 0x400000); // 赤
}
void setup() {
encoder.begin(&Wire, 0x40, 2, 1);
display.init(2, 1);
display.setTextScroll(true);
display.setRotation(1);
//display.setColorDepth(2);
//display.setBrightness(255);
//canvas0.createSprite(240, 135); // 変換画像表示
canvas1.createSprite(240, 176); // カメラ画像表示
Serial.begin();
pinMode(POWER_GPIO_NUM, OUTPUT);
digitalWrite(POWER_GPIO_NUM, LOW);
delay(500);
if (psramFound()) {
size_t psram_size = esp_spiram_get_size() / 1048576;
camera_config.pixel_format = PIXFORMAT_RGB565;
camera_config.fb_location = CAMERA_FB_IN_PSRAM;
camera_config.fb_count = 2;
} else {
Serial.println("PSRAM not found!");
display.println("PSRAM NG!");
delay(500);
}
Serial.begin();
if (!CameraBegin()) {
Serial.println("Camera initialization failed!");
display.println("Camera NG!");
delay(1000);
ESP.restart();
}
Serial.println("Camera initialized...");
display.println("Camera OK!");
// 一度SDカードをマウントして確認
SPI.begin(7, 8, 6, -1);
if (!SD.begin(15, SPI, 80000000)) {
error_lamp();
Serial.println("SD Card initialization failed!");
display.println("SD Card NG!");
delay(500);
return;
} else {
Serial.println("SD Card initialized...");
display.println("SD Card OK!");
// パレット0から7までループ
for (int i = 0; i < 8; i++) {
if (loadPaletteFromSD(i)) {
Serial.printf("Palette %d loaded from SD.\n", i);
display.printf("Palette %d from SD\n", i);
} else {
Serial.printf("Palette %d use default.\n", i);
display.printf("Palette %d default\n", i);
}
delay(100);
}
}
SD.end(); // 一旦ENDしておく
// ボタン押しながら起動で夜間モード
if (!encoder.getButtonStatus()) {
sensor_t* s = esp_camera_sensor_get();
s->set_brightness(s, 1); // -3 to 3 あまり大きくするとノイジーに
s->set_aec2(s, 1); // ナイトモード? 効いてるか微妙
Serial.println("Night Mode");
display.println("Night Mode");
}
delay(500);
encoder.setLEDColor(1, 0x000040); // 青
Serial.println("System initialized...");
display.println("System OK!");
delay(500);
encoder.setLEDColor(1, 0x004000); // 緑
display.fillScreen(TFT_BLACK);
}
void loop() {
newEncoderValue = encoder.getEncoderValue();
bool btn_stauts = encoder.getButtonStatus();
int diff = newEncoderValue - lastEncoderValue;
// 値に変化があった場合のみ処理
if (diff != 0) {
keyOnTime = millis();
accumulatedChange += diff; // 変化量を累積
lastEncoderValue = newEncoderValue; // 今回の値を次回のために保存
// 累積変化量が+2以上になったかチェック
while (accumulatedChange >= 2) {
zoom = zoom + 0.05;
if (zoom > 2) {
zoom = 2; //maxで止まる
}
accumulatedChange -= 2; // 累積変化量から2を引く
}
// 累積変化量が-2以下になったかチェック
while (accumulatedChange <= -2) {
zoom = zoom - 0.05;
if (zoom < 0.5) {
zoom = 0.5; //minで止まる
}
accumulatedChange += 2; // 累積変化量に2を足す
}
/*
if (mode == -1) {
sprintf(currentmode, "MODE:ALL");
} else {
sprintf(currentmode, "MODE:%01d", mode);
currentPalettelndex = mode;
}
display.drawString(currentmode, 10, 54);
*/
}
if (!btn_stauts) {
keyOnTime = millis(); // 最後にボタン操作した時間
encoder.setLEDColor(0, 0x000000); // 一度消灯
encoder.setLEDColor(2, 0x284000); // オレンジ
CameraGet(); // 撮影
SD.end(); // 念のため一旦END
delay(100);
SD.begin(15, SPI, 80000000); // 保存失敗するときは速度を下げる
saveToSD_DisplayBMP(); // ディスプレイの画像保存
sprintf(filesaveprogress, "%04d_Display.bmp", filecounter);
display.drawCenterString(filesaveprogress, 64, 32);
saveToSD_OriginalBMP(); // 変換前の画像保存
sprintf(filesaveprogress, "%04d_Original.bmp", filecounter);
display.drawCenterString(filesaveprogress, 64, 32);
saveGraylevel_fb(); // 輝度情報の保存
if (mode == -1) {
// 全パレット分変換
for (int i = 0; i < maxPalettelndex; i++) {
currentPalettelndex = i;
sprintf(filesaveprogress, "%04d_palette%01d.bmp", filecounter, currentPalettelndex);
display.drawCenterString(filesaveprogress, 64, 32);
saveToSD_ConvertBMP(); // 変換後の画像保存
}
} else {
// モード指定したパレットだけ変換
sprintf(filesaveprogress, "%04d_palette%01d.bmp", filecounter, currentPalettelndex);
display.drawCenterString(filesaveprogress, 64, 32);
saveToSD_ConvertBMP(); // 変換後の画像保存
}
CameraFree(); // フレームバッファを解放
filecounter++; // 連番を更新
SD.end();
encoder.setLEDColor(0, 0x000000); // 一度消灯
encoder.setLEDColor(1, 0x004000); // 緑
}
if ((millis() - keyOnTime >= LED_ON_DURATION)) {
encoder.setLEDColor(0, 0x000000); // LED消灯
display.fillScreen(TFT_BLACK); // ディスプレイ消す
} else {
//display.setBrightness(255);
if (CameraGet()) {
canvas1.pushImage(0, 0, 240, 176, (uint16_t*)fb->buf); // (x, y, w, h, *data)
canvas1.pushRotateZoom(&display, 64, 32, 0, zoom, zoom);
if (zoom == 0.5) {
display.fillRect(0, 0, 4, 64, TFT_BLACK);
display.fillRect(124, 0, 4, 64, TFT_BLACK);
}
CameraFree(); // 取得したフレームを解放
}
}
delay(4);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment