Last active
December 18, 2025 02:11
-
-
Save Yuikawa-Akira/a188db29c5a22e89742360e461bd2d28 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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