Skip to content

Instantly share code, notes, and snippets.

@Yuikawa-Akira
Last active December 18, 2025 02:11
Show Gist options
  • Select an option

  • Save Yuikawa-Akira/a188db29c5a22e89742360e461bd2d28 to your computer and use it in GitHub Desktop.

Select an option

Save Yuikawa-Akira/a188db29c5a22e89742360e461bd2d28 to your computer and use it in GitHub Desktop.
#include <esp_camera.h>
//#include <FastLED.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;
Unit_Encoder encoder; // Unit Encoderのインスタンス
M5UnitLCD display;
//M5UnitGLASS2 display;
M5Canvas canvas_565_p;
M5Canvas canvas_565_v;
M5Canvas canvas_888_p;
M5Canvas canvas_888_v;
M5Canvas canvas_txt;
// 撮影モードの定義
enum ShootingMode {
NORMAL,
BURST,
TIMELAPSE
};
ShootingMode currentMode = NORMAL; // 現在の撮影モード
bool isRecording = false; // 撮影中かどうか
bool isModeChanging = false; // モード切替画面にいるかどうか
bool isFilterEnabled = false; // 新しい状態: フィルタの有効/無効 (デフォルトはOFF)
unsigned long lastCaptureTime = 0; // 前回の撮影時刻 (ms)
const unsigned long TIMELAPSE_INTERVAL = 10000; // タイムラプス間隔 (ms)
unsigned long textDisplayStartTime = 0; // テキストが表示され始めた時刻
const unsigned long TEXT_DISPLAY_DURATION = 1000; // テキストの表示時間 (1000ms = 1秒)
signed short int last_encoder_value = 0;
bool last_btn_status = true; // ボタンの前回状態 (非押下: true, 押下: false)
unsigned long pressStartTime = 0; // ボタンが押され始めた時刻
const unsigned long HOLD_DURATION = 1000; // 長押し判定時間 (ms)
int accumulatedChange = 0; // エンコーダ値の累積変化量
const uint16_t picture_width_pix = 240, picture_height_pix = 176; // 4:3(15:11)
const uint16_t video_width_pix = 128, video_height_pix = 72; // 16:9
char filename[64];
char foldername[64];
char curentpalettesetting[24];
int filecounter = 0;
int foldercounter = 0;
int currentPalettelndex = 0; // 現在のパレットのインデックス
const int maxPalettelndex = 7; // パレット総数
const int ditheringLevels = 8; // ディザ階調数 2以上
uint8_t contrastLUT[256]; // 0〜255の変換結果を保持するテーブル
// デフォルトカラーパレット
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_96X96, // 96x96
//.frame_size = FRAMESIZE_128X128, // 128x128
//.frame_size = FRAMESIZE_QQVGA, // 160x120
//.frame_size = FRAMESIZE_QCIF, // 176x144
.frame_size = FRAMESIZE_HQVGA, // 240x176
//.frame_size = FRAMESIZE_240X240, // 240x240
//.frame_size = FRAMESIZE_QVGA, // 320x240
.jpeg_quality = 0,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_LATEST,
.sccb_i2c_port = 0,
};
void loadFolderCounter_internal() {
if (SD.exists("/config.txt")) {
File file = SD.open("/config.txt", FILE_READ);
if (file) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
int loaded_count = line.toInt();
foldercounter = loaded_count % 10000; // 読み込んだ値を0-9999の範囲に制限して格納
Serial.printf("Loaded foldercounter: %d (Clamped to: %d)\n", loaded_count, foldercounter);
} else {
foldercounter = 0; // ファイルが空の場合は0
Serial.println("config.txt is empty. Setting foldercounter to 0.");
}
file.close();
} else {
Serial.println("ERROR: Failed to open config.txt for reading. Setting foldercounter to 0.");
foldercounter = 0;
}
} else {
// config.txtが存在しない場合、作成して0を書き込む
File file = SD.open("/config.txt", FILE_WRITE);
if (file) {
file.println(0);
file.close();
Serial.println("Created new config.txt with foldercounter 0");
} else {
Serial.println("ERROR: Failed to create config.txt.");
errorLed();
}
foldercounter = 0;
}
}
// SDカードが既にマウントされていることを前提として、foldercounterをconfig.txtに書き込む
void writeFolderCounter_internal(int count) {
// 'w' (FILE_WRITE)で開くと、ファイルが存在する場合は内容がクリアされる
File file = SD.open("/config.txt", FILE_WRITE);
if (file) {
// 【修正箇所: 0-9999でループするように制限してから書き込み】
int save_count = count % 10000;
file.println(save_count);
file.close();
Serial.printf("Saved foldercounter: %d to config.txt\n", save_count);
} else {
Serial.println("ERROR: Failed to open config.txt for writing (SD mounted).");
errorLed();
}
}
bool loadPaletteFromSD(int paletteIndex) {
if (paletteIndex < 0 || paletteIndex > maxPalettelndex) {
return false;
}
String filename = "/ColorPalette" + String(paletteIndex) + ".txt"; // ファイル名を生成 (例: /ColorPalette0.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_hmirror(s, 1); //左右反転 0無効 1有効
s->set_vflip(s, 1); //上下反転 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;
}
uint16_t swap16(uint16_t value) {
return (value << 8) | (value >> 8);
}
void applyDitherAndPalette_565to888_Fast(M5Canvas& srcSprite, M5Canvas& dstSprite, int levelsPerChannel, int paletteIndex) {
// =======================================================
// 1. 定数定義と初期化
// =======================================================
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 BAYER_DENOMINATOR = 16.0f;
const int width = dstSprite.width();
const int height = dstSprite.height();
const int total_pixels = width * height;
// パレットインデックスの範囲チェックとポインタ設定
if (paletteIndex < 0 || paletteIndex > maxPalettelndex) {
paletteIndex = 0;
}
const uint32_t* targetPalette = ColorPalettes[paletteIndex];
// M5Canvasのメモリバッファへ直接アクセス
uint16_t* src_buffer = (uint16_t*)srcSprite.getBuffer(); // RGB565
uint8_t* dst_buffer = (uint8_t*)dstSprite.getBuffer(); // RGB888
if (!src_buffer || !dst_buffer) {
Serial.println("Error: Canvas buffer not accessible.");
errorLed();
return;
}
// =======================================================
// 2. 【最適化】levelsPerChannelが0または1の場合 (スキップパス)
// =======================================================
if (levelsPerChannel <= 1) {
for (int i = 0; i < total_pixels; ++i) {
uint16_t rgb565Color = swap16(src_buffer[i]);
// RGB565 -> RGB888 変換
uint8_t r = (rgb565Color >> 11) << 3;
r |= r >> 5;
uint8_t g = ((rgb565Color >> 5) & 0x3F) << 2;
g |= g >> 6;
uint8_t b = (rgb565Color & 0x1F) << 3;
b |= b >> 5;
// 輝度の計算 (BT.709係数に基づく)
// Y = (54 * R + 183 * G + 19 * B) / 256
uint32_t luminance_int = (54U * r + 183U * g + 19U * b) >> 8;
// 輝度を8階調 (0-7)に量子化 (256/32 = 8)
uint8_t grayLevel = luminance_int >> 5;
if (grayLevel > 7) grayLevel = 7;
// パレットルックアップ
uint32_t paletted_color = targetPalette[grayLevel];
// RGB888 (3バイト) として書き込み (R, G, Bの順を仮定)
dst_buffer[i * 3 + 0] = (paletted_color >> 16) & 0xFF; // R
dst_buffer[i * 3 + 1] = (paletted_color >> 8) & 0xFF; // G
dst_buffer[i * 3 + 2] = paletted_color & 0xFF; // B
}
return; // 処理終了
}
// =======================================================
// 3. 【ディザ処理メインループ】levelsPerChannel > 1 の場合
// =======================================================
// 0から255までの各チャンネルのステップサイズ (float)
const float step = 255.0f / (float)(levelsPerChannel - 1);
for (int y = 0; y < height; ++y) {
const int y_idx = y & 3;
// 行ごとのバッファ開始ポインタを計算
uint16_t* current_src_ptr = src_buffer + (ptrdiff_t)y * width;
uint8_t* current_dst_ptr = dst_buffer + (ptrdiff_t)y * width * 3;
for (int x = 0; x < width; ++x) {
const int x_idx = x & 3;
const uint8_t bayerValue = bayer4x4[y_idx][x_idx];
// 【読み込みとRGB888成分の抽出】
uint16_t rgb565Color = swap16(*current_src_ptr);
uint8_t r_src = (rgb565Color >> 11) << 3;
r_src |= r_src >> 5;
uint8_t g_src = ((rgb565Color >> 5) & 0x3F) << 2;
g_src |= g_src >> 6;
uint8_t b_src = (rgb565Color & 0x1F) << 3;
b_src |= b_src >> 5;
uint8_t channels_src[3] = { r_src, g_src, b_src };
uint8_t channels_dithered[3];
// 【ディザリング適用】
for (int ch = 0; ch < 3; ++ch) {
const uint8_t val_src = channels_src[ch];
int level_index = (int)std::floorf((float)val_src / step);
if (level_index >= levelsPerChannel - 1) {
level_index = levelsPerChannel - 2;
}
const float level_low = (float)level_index * step;
const float error = (float)val_src - level_low;
uint8_t val_dithered;
// 浮動小数点数の乗算ベース比較によるディザリング決定
if (error * BAYER_DENOMINATOR >= (float)bayerValue * step) {
val_dithered = (uint8_t)std::roundf(level_low + step);
} else {
val_dithered = (uint8_t)std::roundf(level_low);
}
// 最終的なディザリング出力値は0-255の範囲内にクランプ
channels_dithered[ch] = std::max(0, std::min(255, (int)val_dithered));
}
// 【パレット変換】ディザリング後の色を使って輝度を計算
uint8_t r_dst = channels_dithered[0];
uint8_t g_dst = channels_dithered[1];
uint8_t b_dst = channels_dithered[2];
uint32_t luminance_int = (54U * r_dst + 183U * g_dst + 19U * b_dst) >> 8;
uint8_t grayLevel = luminance_int >> 5;
if (grayLevel > 7) grayLevel = 7;
// パレットルックアップ
uint32_t paletted_color = targetPalette[grayLevel];
// 【書き込み】RGB888 (3バイト) をバッファに直接書き込む
*current_dst_ptr++ = (paletted_color >> 16) & 0xFF; // R
*current_dst_ptr++ = (paletted_color >> 8) & 0xFF; // G
*current_dst_ptr++ = paletted_color & 0xFF; // B
// srcポインタを次のピクセルへ進める
current_src_ptr++;
}
}
}
bool savePNGToSD(M5Canvas& canvasRef, const char* filename) {
// 1. PNGデータの生成準備
//Serial.printf("Creating PNG data in memory for canvas (W:%d, H:%d)...\n", canvasRef.width(), canvasRef.height());
size_t datalen = 0;
// 2. createPngを呼び出し、PNGデータをメモリに作成
void* pngData = canvasRef.createPng(&datalen, 0, 0, canvasRef.width(), canvasRef.height());
if (pngData == nullptr || datalen == 0) {
Serial.printf("ERROR: Failed to create PNG data in memory (datalen: %u).\n", datalen);
errorLed();
return false;
}
//Serial.printf("PNG data created successfully (Size: %u bytes). Opening file: %s\n", datalen, filename);
bool result = false;
// 3. SDカードにファイルをオープン
File file = SD.open(filename, "w");
if (file) {
// 4. メモリ上のPNGデータをファイルに書き込み
size_t writtenBytes = file.write((const uint8_t*)pngData, datalen);
file.close();
if (writtenBytes == datalen) {
//Serial.printf("Successfully saved PNG file: %s (Wrote %u bytes)\n", filename, writtenBytes);
result = true;
} else {
Serial.printf("ERROR: Failed to write full PNG data (Expected: %u, Wrote: %u)\n", datalen, writtenBytes);
errorLed();
}
} else {
Serial.printf("ERROR: Could not open file for writing: %s\n", filename);
errorLed();
}
// 5. createPngで確保されたメモリを解放
free(pngData);
return result;
}
String getModeName(ShootingMode mode) {
switch (mode) {
case NORMAL: return "NORMAL";
case BURST: return "BURST";
case TIMELAPSE: return "TIMELAPSE";
default: return "UNKNOWN";
}
}
void setup() {
display.init(2, 1);
display.setTextScroll(true);
//display.setRotation(1);
display.setRotation(3);
display.setColorDepth(2);
delay(10);
encoder.begin(&Wire, 0x40, 2, 1);
Serial.begin(115200);
pinMode(POWER_GPIO_NUM, OUTPUT);
digitalWrite(POWER_GPIO_NUM, LOW);
delay(500);
canvas_565_v.setColorDepth(16);
canvas_565_v.createSprite(video_width_pix, video_height_pix);
canvas_565_v.setPsram(false);
canvas_txt.setColorDepth(1);
canvas_txt.createSprite(128, 12);
initCanvas_v();
initCanvas_p(); // 初回なぜか失敗することがあるので初期化しておく
// 一度SDカードをマウントして確認
SPI.begin(7, 8, 6, -1);
if (!SD.begin(15, SPI, 10000000)) {
//FastLED.show(); // エラー
Serial.println("SD Card initialization failed!");
display.println("SD Card NG!");
errorLed();
delay(500);
return;
} else {
Serial.println("SD Card initialized...");
display.println("SD Card OK!");
loadFolderCounter_internal();
// パレット0から7までループ
for (int i = 0; i <= maxPalettelndex; 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 (psramFound()) {
camera_config.pixel_format = PIXFORMAT_RGB565;
camera_config.fb_location = CAMERA_FB_IN_PSRAM;
camera_config.fb_count = 2;
} else {
errorLed();
Serial.println("PSRAM not found!");
display.println("PSRAM NG!");
delay(500);
}
if (!CameraBegin()) {
errorLed();
Serial.println("Camera initialization failed!");
display.println("Camera NG!");
delay(1000);
ESP.restart();
}
delay(500);
Serial.println("Camera initialized...");
display.println("Camera OK!");
updateContrastTable(0.63f);
delay(500);
display.clear(TFT_BLACK);
}
void loop() {
if (isModeChanging) {
// --- モード切替画面 ---
canvas_565_v.clear(TFT_BLACK);
canvas_565_v.drawCenterString("--- MODE SELECT ---", 64, 10);
int y = 24;
for (int i = 0; i <= TIMELAPSE; i++) {
String modeName = getModeName((ShootingMode)i);
if ((ShootingMode)i == currentMode) {
canvas_565_v.fillRect(0, y, 128, 16, TFT_BLACK);
canvas_565_v.drawCenterString(">> " + modeName + " <<", 64, y); // 選択中のモードをハイライト
} else {
canvas_565_v.drawCenterString(modeName, 64, y);
}
y += 16;
}
} else {
// --- 通常画面 ---
CameraGet();
canvas_565_v.pushImageRotateZoom(0, 0, 0, 16, 0, 0.534, 0.5, picture_width_pix, picture_height_pix - 16, (uint16_t*)fb->buf);
CameraFree();
}
// --------------------------------------------------
// A. エンコーダ入力の取得と状態管理
// --------------------------------------------------
signed short int encoder_value = encoder.getEncoderValue();
bool btn_status = encoder.getButtonStatus(); // ボタンの状態 (押下: false, 非押下: true)
signed short int delta = encoder_value - last_encoder_value; // 回転量 (Delta) の計算
// --------------------------------------------------
// B. 回転操作 (ロータリー) の処理
// --------------------------------------------------
if (delta != 0) {
accumulatedChange += delta;
last_encoder_value = encoder_value;
if (isModeChanging) {
// モード切替画面 (長押し後)
int modeInt = (int)currentMode;
while (accumulatedChange >= 2) {
modeInt++; // モードを+1
if (modeInt > 2) {
modeInt = 2;
}
accumulatedChange -= 2;
}
while (accumulatedChange <= -2) {
modeInt--; // モードを-1
if (modeInt < 0) {
modeInt = 0;
}
accumulatedChange += 2;
}
currentMode = (ShootingMode)modeInt;
} else if (currentMode == BURST || currentMode == TIMELAPSE) {
int nextPalette = currentPalettelndex;
while (accumulatedChange >= 2) {
nextPalette++; // モードを+1
if (nextPalette > maxPalettelndex) {
nextPalette = 0; // 0に戻る
isFilterEnabled = !isFilterEnabled; // OFF/ONをトグル
}
accumulatedChange -= 2;
}
while (accumulatedChange <= -2) {
nextPalette--; // モードを-1
if (nextPalette < 0) {
nextPalette = maxPalettelndex; // maxに戻る
isFilterEnabled = !isFilterEnabled;
}
accumulatedChange += 2;
}
currentPalettelndex = nextPalette;
if (isFilterEnabled == false) {
Serial.printf("Palette %d D-OFF\n", currentPalettelndex);
sprintf(curentpalettesetting, "Palette %d D-OFF", currentPalettelndex);
} else {
Serial.printf("Palette %d D-ON\n", currentPalettelndex);
sprintf(curentpalettesetting, "Palette %d D-ON", currentPalettelndex);
}
textDisplayStartTime = millis();
}
}
// --------------------------------------------------
// C. クリック操作 (短押し・長押し) の処理
// --------------------------------------------------
if (!btn_status && last_btn_status) {
// ボタンが押され始めた瞬間 (立ち下がりエッジ)
pressStartTime = millis();
}
// 長押し判定 (長押し検出後、ボタンを離すまで繰り返し判定しないように注意)
bool isLongPress = !btn_status && (millis() - pressStartTime >= HOLD_DURATION);
if (isLongPress && !isModeChanging) {
// --- 長押し: モード切替画面への移行 (全モードで有効) ---
display.clear(TFT_BLACK);
isModeChanging = true;
isRecording = false;
pressStartTime = 0; // 長押し判定をリセット
cleanupProcess();
Serial.println("--- MODECHANGING ---");
} else if (btn_status && !last_btn_status) {
// ボタンが離された瞬間 (立ち上がりエッジ)
unsigned long pressDuration = millis() - (pressStartTime > 0 ? pressStartTime : 0);
if (pressDuration < HOLD_DURATION) {
// --- 短いクリック判定 ---
if (isModeChanging) {
// 1. モード切替画面でのクリック: モード確定
isModeChanging = false;
Serial.printf("Current Mode: %s\n", getModeName(currentMode).c_str());
} else {
// 2. 通常画面でのクリック: 撮影開始/停止
initializeProcess();
if (currentMode == NORMAL) {
// ノーマルモード: 開始のみ
if (!isRecording) {
isRecording = true;
Serial.println("--- Start Recording (NORMAL) ---");
executeSingleProcess();
cleanupProcess();
isRecording = false;
Serial.println("--- Stop Recording (NORMAL) ---");
}
} else if (currentMode == BURST || currentMode == TIMELAPSE) {
// バースト/タイムラプス: トグル
isRecording = !isRecording;
if (isRecording) {
makeDir();
initCanvas_v();
Serial.println("--- Start Recording (BURST/TIMELAPSE) ---");
} else {
Serial.println("--- Stop Recording (BURST/TIMELAPSE) ---");
cleanupProcess();
}
}
}
}
pressStartTime = 0; // 短押し処理後、タイマーリセット
}
// 次のループのためのボタン状態を保存
last_btn_status = btn_status;
if (currentMode == BURST && isRecording == true) {
executeBurstProcess();
}
if (currentMode == TIMELAPSE && isRecording == true) {
executeTimelapseProcess();
}
if (textDisplayStartTime > 0) {
if (millis() - textDisplayStartTime < TEXT_DISPLAY_DURATION) {
canvas_txt.clear(TFT_BLACK);
canvas_txt.drawCenterString(curentpalettesetting, 64, 2);
} else {
canvas_txt.clear(TFT_BLACK);
textDisplayStartTime = 0;
}
canvas_txt.pushSprite(&canvas_565_v, 0, 56);
}
//canvas_565_v.pushSprite(&display, 0, -4);
canvas_565_v.pushRotateZoom(&display, 120, 67, 0.0, 1.875, 1.875);
setModeLED();
}
void initCanvas_p() {
canvas_565_p.setColorDepth(16);
canvas_565_p.createSprite(picture_width_pix, picture_height_pix);
canvas_565_p.setPsram(false);
canvas_888_p.setColorDepth(24);
canvas_888_p.createSprite(picture_width_pix, picture_height_pix);
canvas_888_p.setPsram(true);
}
void initCanvas_v() {
canvas_888_v.setColorDepth(24);
canvas_888_v.createSprite(video_width_pix, video_height_pix);
canvas_888_v.setPsram(false);
}
// 処理開始時に一度だけ実行される関数
void initializeProcess() {
Serial.println(">>> Process STARTED! <<<");
SD.begin(15, SPI, 10000000);
lastCaptureTime = millis();
}
void makeDir() {
sprintf(foldername, "/%04d", foldercounter);
SD.mkdir(foldername);
}
// NORMAL撮影
void executeSingleProcess() {
initCanvas_p();
CameraGet();
canvas_565_p.pushImage(0, 0, picture_width_pix, picture_height_pix, (uint16_t*)fb->buf);
CameraFree();
for (int i = 0; i <= maxPalettelndex; i++) {
currentPalettelndex = i;
applyDitherAndPalette_565to888_Fast(canvas_565_p, canvas_888_p, 0, currentPalettelndex);
sprintf(filename, "/%04d_p%02d.png", foldercounter, currentPalettelndex);
display.drawCenterString(filename, 64, 32);
savePNGToSD(canvas_888_p, filename);
}
for (int i = 0; i <= maxPalettelndex; i++) {
currentPalettelndex = i;
applyDitherAndPalette_565to888_Fast(canvas_565_p, canvas_888_p, ditheringLevels, currentPalettelndex);
sprintf(filename, "/%04d_d%02d.png", foldercounter, currentPalettelndex);
display.drawCenterString(filename, 64, 32);
savePNGToSD(canvas_888_p, filename);
}
//float scale = 0.0f;
//for (int i = 0; i <= 10; i++) {
//digital8
//scale = 0.01f * i + 0.6f;
//updateContrastTable(scale);
applyDitherAndPalette_565to888_Fast_digital8(canvas_565_p, canvas_888_p);
//sprintf(filename, "/%04d_pc8_%02d.png", foldercounter, i);
sprintf(filename, "/%04d_pc8.png", foldercounter);
display.drawCenterString(filename, 64, 32);
savePNGToSD(canvas_888_p, filename);
//}
//raw
sprintf(filename, "/%04d_raw.png", foldercounter);
display.drawCenterString(filename, 64, 32);
savePNGToSD(canvas_565_p, filename);
currentPalettelndex = 0;
canvas_565_p.deleteSprite();
canvas_888_p.deleteSprite();
}
// BURST撮影
void executeBurstProcess() {
CameraGet();
canvas_565_v.pushImageRotateZoom(0, 0, 0, 16, 0, 0.534, 0.5, picture_width_pix, picture_height_pix - 16, (uint16_t*)fb->buf);
CameraFree();
int ditherLevel = isFilterEnabled ? ditheringLevels : 0;
applyDitherAndPalette_565to888_Fast(canvas_565_v, canvas_888_v, ditherLevel, currentPalettelndex);
sprintf(filename, "%s/v%06d.png", foldername, filecounter);
savePNGToSD(canvas_888_v, filename);
filecounter++; // 連番を更新
}
// TIMELAPSE撮影
void executeTimelapseProcess() {
// 1000ms間隔のタイマーチェック
if (millis() - lastCaptureTime >= TIMELAPSE_INTERVAL) {
initCanvas_p();
CameraGet();
canvas_565_p.pushImage(0, 0, picture_width_pix, picture_height_pix, (uint16_t*)fb->buf);
CameraFree(); // フレームバッファを解放
int ditherLevel = isFilterEnabled ? ditheringLevels : 0;
applyDitherAndPalette_565to888_Fast(canvas_565_p, canvas_888_p, ditherLevel, currentPalettelndex);
// フォルダ内に連番で保存: /0001/t000001.png
sprintf(filename, "%s/t%06d.png", foldername, filecounter);
Serial.printf("Saving Timelapse: %s\n", filename);
if (savePNGToSD(canvas_888_p, filename)) {
filecounter++; // 連番を更新
} else {
errorLed();
}
// 5. Canvasを破棄してメモリを解放
canvas_565_p.deleteSprite();
canvas_888_p.deleteSprite();
// 6. 撮影時刻を更新
lastCaptureTime = millis();
}
}
// 処理が停止中に繰り返し実行される関数
void idleProcess() {
}
// 処理停止時に一度だけ実行される関数
void cleanupProcess() {
Serial.println(">>> Process STOPPED! <<<");
foldercounter = (foldercounter + 1) % 10000;
writeFolderCounter_internal(foldercounter);
SD.end();
filecounter = 0;
canvas_888_v.deleteSprite();
canvas_888_p.deleteSprite();
encoder.setLEDColor(0, 0x000000);
}
void errorLed() {
encoder.setLEDColor(0, 0xFF0000); // Red
}
void setModeLED() {
if (isModeChanging) {
encoder.setLEDColor(0, 0x000000); // Black
} else {
switch (currentMode) {
case NORMAL:
encoder.setLEDColor(0, 0x00FFFF); // Cyan
break;
case BURST:
encoder.setLEDColor(0, 0x0000FF); // Blue
break;
case TIMELAPSE:
encoder.setLEDColor(0, 0x008000); // Green
break;
}
}
if (isRecording) {
encoder.setLEDColor(0, 0xFFA500); // Orange
}
}
const uint32_t FixedColorPalette[] = {
0x000000, // Black
0x0000FF, // Blue
0x00FF00, // Green
0x00FFFF, // Cyan
0xFF0000, // Red
0xFF00FF, // Magenta
0xFFFF00, // Yellow
0xFFFFFF // White
};
const size_t FixedPaletteSize = 8;
/**
* @brief 2つの色の加重二乗ユークリッド距離を計算します。
* 重み: R=2, G=4, B=3
*/
long calculate_weighted_distance_sq(uint8_t r1, uint8_t g1, uint8_t b1, uint8_t r2, uint8_t g2, uint8_t b2) {
// long型で差分を計算し、オーバーフローを防ぐ
long dr = (long)r1 - (long)r2;
long dg = (long)g1 - (long)g2;
long db = (long)b1 - (long)b2;
// 加重二乗距離を計算
// R: x2, G: x4, B: x3
long distance_sq =
2 * (dr * dr) + 4 * (dg * dg) + 3 * (db * db);
return distance_sq;
}
/**
* @brief 入力RGB値に対し、固定パレット内で最も近い色を見つけ、その色に置き換えます。
* 距離計算には加重ユークリッド距離 (R:2, G:4, B:3) を使用します。
* * @param r 入力R値 (0-255)
* @param g 入力G値 (0-255)
* @param b 入力B値 (0-255)
* @param out_r 変換後のR値の格納先
* @param out_g 変換後のG値の格納先
* @param out_b 変換後のB値の格納先
*/
void quantize_color_to_closest(uint8_t r, uint8_t g, uint8_t b, uint8_t* out_r, uint8_t* out_g, uint8_t* out_b) {
long min_distance_sq = -1;
uint32_t closest_color = 0;
// 1. パレット全体を走査し、最小距離の色を探索
for (size_t i = 0; i < FixedPaletteSize; ++i) {
uint32_t palette_color = FixedColorPalette[i];
// パレット色からRGB成分を分解 (0xRRGGBB形式)
uint8_t r_pal = (palette_color >> 16) & 0xFF;
uint8_t g_pal = (palette_color >> 8) & 0xFF;
uint8_t b_pal = palette_color & 0xFF;
// 2. 加重二乗距離を計算
long distance_sq = calculate_weighted_distance_sq(
r, g, b,
r_pal, g_pal, b_pal);
// 3. 最小距離の更新
if (min_distance_sq == -1 || distance_sq < min_distance_sq) {
min_distance_sq = distance_sq;
closest_color = palette_color;
}
}
// 4. 最も近い色を分解して出力に格納
*out_r = (closest_color >> 16) & 0xFF;
*out_g = (closest_color >> 8) & 0xFF;
*out_b = closest_color & 0xFF;
}
void applyDitherAndPalette_565to888_Fast_digital8(M5Canvas& srcSprite, M5Canvas& dstSprite) {
// 1. 定数定義と初期化
const int width = dstSprite.width();
const int height = dstSprite.height();
const int total_pixels = width * height;
const uint8_t BAYER_MATRIX_SCALED[4][4] = { { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, { 3, 11, 1, 9 }, { 15, 7, 13, 5 } };
const uint8_t BAYER_SIZE = 4;
const uint8_t QUANT_SHIFT = 5;
const uint8_t MAX_8BIT = 255;
const uint8_t BIAS_LEVEL = 2;
// M5Canvasのメモリバッファへ直接アクセス
uint16_t* src_buffer = (uint16_t*)srcSprite.getBuffer(); // RGB565
uint8_t* dst_buffer = (uint8_t*)dstSprite.getBuffer(); // RGB888
if (!src_buffer || !dst_buffer) {
Serial.println("Error: Canvas buffer not accessible.");
errorLed();
return;
}
// 3. 【ディザ処理メインループ】
for (int y = 0; y < height; ++y) {
const int y_idx = y & 3;
// 行ごとのバッファ開始ポインタを計算
uint16_t* current_src_ptr = src_buffer + (ptrdiff_t)y * width;
uint8_t* current_dst_ptr = dst_buffer + (ptrdiff_t)y * width * 3;
for (int x = 0; x < width; ++x) {
const int x_idx = x & 3;
// 【読み込みとRGB888成分の抽出】
uint16_t rgb565Color = swap16(*current_src_ptr);
uint8_t r_src = (rgb565Color >> 11) << 3;
r_src |= r_src >> 5;
uint8_t g_src = ((rgb565Color >> 5) & 0x3F) << 2;
g_src |= g_src >> 6;
uint8_t b_src = (rgb565Color & 0x1F) << 3;
b_src |= b_src >> 5;
//processRGB888_ReverseSigmoidal(r_src, g_src, b_src, 1.25f); // 明度補正
//adjustContrast(r_src, g_src, b_src, 0.4f);
applyContrastLUT(r_src, g_src, b_src);
float h, s, v;
rgbToHsv(r_src, g_src, b_src, h, s, v);
s = std::min(s * 2.0f, 1.0f); // 彩度アップ
hsvToRgb(h, s, v, r_src, g_src, b_src);
// 【ディザリング適用】
uint8_t bayer_val = BAYER_MATRIX_SCALED[y % BAYER_SIZE][x % BAYER_SIZE];
uint8_t r_out_888, g_out_888, b_out_888;
// Rチャンネル
uint16_t r_temp = (uint16_t)r_src + bayer_val * BIAS_LEVEL;
r_temp = (r_temp > MAX_8BIT) ? MAX_8BIT : r_temp; // 255にクランプ
uint8_t r_level = r_temp >> QUANT_SHIFT; // 量子化 (上位3bitを取得, 0-7)
r_out_888 = r_level * 36; // 0x24 = 36 で乗算 (近似)
// Gチャンネル
uint16_t g_temp = (uint16_t)g_src + bayer_val * BIAS_LEVEL;
g_temp = (g_temp > MAX_8BIT) ? MAX_8BIT : g_temp;
uint8_t g_level = g_temp >> QUANT_SHIFT;
g_out_888 = g_level * 36;
// Bチャンネル
uint16_t b_temp = (uint16_t)b_src + bayer_val * BIAS_LEVEL;
b_temp = (b_temp > MAX_8BIT) ? MAX_8BIT : b_temp;
uint8_t b_level = b_temp >> QUANT_SHIFT;
b_out_888 = b_level * 36;
quantize_color_to_closest(r_out_888, g_out_888, b_out_888, &r_out_888, &g_out_888, &b_out_888);
// 【書き込み】RGB888 (3バイト) をバッファに直接書き込む
*current_dst_ptr++ = r_out_888; // R
*current_dst_ptr++ = g_out_888; // G
*current_dst_ptr++ = b_out_888; // B
// srcポインタを次のピクセルへ進める
current_src_ptr++;
}
}
}
float applySigmoid(float x, float gain, float mid_point = 0.5f) {
// ステップ1: xを中心点からのシフトを行う
float shifted_x = x - mid_point;
// ステップ2: シグモイド関数を適用
// 通常のシグモイド関数: 1.0 / (1.0 + exp(-gain * shifted_x))
float sigmoid_output = 1.0f / (1.0f + std::exp(-gain * shifted_x));
// ステップ3: 出力を 0.0 から 1.0 の範囲に正規化
return sigmoid_output;
}
/**
* @brief RGB (0-255) から HSV (H:0-360, S,V:0.0-1.0) への変換を行います。
* * @param r_in 入力R値 (0-255)
* @param g_in 入力G値 (0-255)
* @param b_in 入力B値 (0-255)
* @param h_out 色相H (0.0-360.0) の出力参照
* @param s_out 彩度S (0.0-1.0) の出力参照
* @param v_out 明度V (0.0-1.0) の出力参照
*/
void rgbToHsv(uint8_t r_in, uint8_t g_in, uint8_t b_in, float& h_out, float& s_out, float& v_out) {
// RGB (0-255) を正規化された R'G'B' (0.0-1.0) に変換
float r_prime = r_in / 255.0f;
float g_prime = g_in / 255.0f;
float b_prime = b_in / 255.0f;
float c_max = std::max({ r_prime, g_prime, b_prime }); // V (明度)
float c_min = std::min({ r_prime, g_prime, b_prime });
float delta = c_max - c_min; // C (クロマ)
v_out = c_max;
// 彩度 (S) の計算
if (c_max > 0.0f) {
s_out = delta / c_max; // S = C / V
} else {
s_out = 0.0f;
}
// 色相 (H) の計算
if (delta == 0.0f) {
h_out = 0.0f; // 無彩色の場合、Hは0
} else {
if (c_max == r_prime) {
h_out = 60.0f * (fmodf((g_prime - b_prime) / delta, 6.0f));
} else if (c_max == g_prime) {
h_out = 60.0f * (((b_prime - r_prime) / delta) + 2.0f);
} else if (c_max == b_prime) {
h_out = 60.0f * (((r_prime - g_prime) / delta) + 4.0f);
}
}
// Hが負の値になった場合、360を加算して0-360の範囲に収める
if (h_out < 0.0f) {
h_out += 360.0f;
}
}
/**
* @brief HSV (H:0-360, S,V:0.0-1.0) から RGB (0-255) への変換を行います。
* * @param h_in 入力色相H (0.0-360.0)
* @param s_in 入力彩度S (0.0-1.0)
* @param v_in 入力明度V (0.0-1.0)
* @param r_out 出力R値 (0-255) の出力参照
* @param g_out 出力G値 (0-255) の出力参照
* @param b_out 出力B値 (0-255) の出力参照
*/
void hsvToRgb(float h_in, float s_in, float v_in, uint8_t& r_out, uint8_t& g_out, uint8_t& b_out) {
// S=0 (無彩色) の場合、R=G=B=V
if (s_in == 0.0f) {
uint8_t gray = (uint8_t)std::clamp(std::round(v_in * 255.0f), 0.0f, 255.0f);
r_out = g_out = b_out = gray;
return;
}
// S > 0 の場合
float c = v_in * s_in; // クロマ C
float h_prime = fmodf(h_in / 60.0f, 6.0f); // H' = H / 60 (0.0〜6.0)
float x = c * (1.0f - std::abs(fmodf(h_prime, 2.0f) - 1.0f));
float r_tmp = 0.0f, g_tmp = 0.0f, b_tmp = 0.0f;
// H' の値に応じて R, G, B の一時値を計算 (R'G'B' の中間値)
if (h_prime >= 0.0f && h_prime < 1.0f) {
r_tmp = c;
g_tmp = x;
b_tmp = 0.0f;
} else if (h_prime >= 1.0f && h_prime < 2.0f) {
r_tmp = x;
g_tmp = c;
b_tmp = 0.0f;
} else if (h_prime >= 2.0f && h_prime < 3.0f) {
r_tmp = 0.0f;
g_tmp = c;
b_tmp = x;
} else if (h_prime >= 3.0f && h_prime < 4.0f) {
r_tmp = 0.0f;
g_tmp = x;
b_tmp = c;
} else if (h_prime >= 4.0f && h_prime < 5.0f) {
r_tmp = x;
g_tmp = 0.0f;
b_tmp = c;
} else { // 5.0 <= h_prime < 6.0
r_tmp = c;
g_tmp = 0.0f;
b_tmp = x;
}
// m (ライトネスシフト) を計算し、R, G, B の一時値に加算して最終的な R'G'B' を得る
float m = v_in - c;
float r_final = r_tmp + m;
float g_final = g_tmp + m;
float b_final = b_tmp + m;
// 逆正規化 (0.0-1.0 -> 0-255) とクランプ/丸め
r_out = (uint8_t)std::clamp(std::round(r_final * 255.0f), 0.0f, 255.0f);
g_out = (uint8_t)std::clamp(std::round(g_final * 255.0f), 0.0f, 255.0f);
b_out = (uint8_t)std::clamp(std::round(b_final * 255.0f), 0.0f, 255.0f);
}
void updateContrastTable(float strength) {
// strengthを0.0〜1.0にクランプ(安全のため)
strength = constrain(strength, 0.0f, 1.0f);
for (int i = 0; i < 256; i++) {
// 1. 0.0〜1.0に正規化
float x = i / 255.0f;
// 2. 基本となる逆S字 (Smoothstepの逆)
// これにより、両端(白・黒)付近の階調を中央へ押し出す
float sCurve = 3 * x * x - 2 * x * x * x;
float invS = x + (sCurve - x) * -0.5f; // 軽く逆S字をかける
// 3. ターゲットとなる「完全なグレー(0.5)」と、現在の値(invS)を線形補完
// strength=1.0 で、すべての値が 0.5 (128) に集約される
float target = 0.46f; // やや暗い方よりに調整
float result = invS + (target - invS) * strength;
// 4. 0-255に戻して格納
contrastLUT[i] = (uint8_t)constrain(result * 255.0f, 0, 255);
}
}
void applyContrastLUT(uint8_t& r, uint8_t& g, uint8_t& b) {
r = contrastLUT[r];
g = contrastLUT[g];
b = contrastLUT[b];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment