Skip to content

Instantly share code, notes, and snippets.

@Yuikawa-Akira
Last active August 30, 2026 09:49
Show Gist options
  • Select an option

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

Select an option

Save Yuikawa-Akira/7e2eaef2d79a73e506a7a853aa58a739 to your computer and use it in GitHub Desktop.
AtomS3R Pixel-art style camera
/*
MIT License
Copyright (c) 2026 Yuikawa-Akira
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <esp_camera.h>
#include <SPI.h>
#include <SD.h>
#include <M5Unified.h>
#define KEY_PIN 1
#define LED_PIN 2
#define POWER_GPIO_NUM 18
camera_fb_t* fb;
M5Canvas canvas_565_p;
const int NUM_PALETTES = 8;
M5Canvas dstSprites[NUM_PALETTES];
const uint16_t PICTURE_WIDTH_PIX = 240, PICTURE_HEIGHT_PIX = 176; // 4:3(15:11)
char filename[64];
char foldername[64];
char currentPaletteSetting[24];
int fileCounter = 0;
int folderCounter = 0;
int currentPaletteIndex = 0; // 現在選択中のパレットインデックス (0〜7:通常パレット, 8:digital8)
const int maxPaletteIndex = 7; // 通常カラーパレットの総数 (ColorPalettes[8][8] のインデックス上限)
const int DIGITAL8_PALETTE_INDEX = 8; // currentPaletteIndex がこの値のときdigital8処理を使う
const int maxPaletteSelectIndex = 8; // パレット選択の上限 (0〜7:通常パレット, 8:digital8)
int ditheringLevels = 8; // ディザ階調数 2以上 (SDカードの /Dither8.txt から上書き可能)
bool enableDithering = true; // ディザリング処理の有無 (SDカードの /Dither8.txt から上書き可能。デフォルトtrue)
uint8_t contrastLUT[256]; // 0〜255の変換結果を保持するテーブル
uint16_t d8_satFactorX128 = 256; // 彩度倍率 x128 (128=1.0倍, 256=2.0倍)
float d8_sigmoidStrength = 24.0f; // sigmoidal-contrastの強度
float d8_sigmoidMidpoint = 0.50f; // sigmoidal-contrastの中心点 (0.0〜1.0)
int16_t d8_biasWidth = 12; // Bayerディザの中心化バイアス幅 (±)
// TailBATを使用しているとき、消費電流が45mA以下だと電源がシャットダウンしてしまう対策
uint32_t LED_ON_DURATION = 120000; // LED 点灯時間 (ミリ秒)
uint32_t keyOnTime = 0; // キースイッチを操作した時間
// カラーパレット
uint32_t ColorPalettes[8][8] = {
{ // パレット0 SLSO8 Created by Luis Miguel Maldonado
0x0D2B45, 0x203C56, 0x544E68, 0x8D697A, 0xD08159, 0xFFAA5E, 0xFFD4A3, 0xFFECD6 },
{ // パレット1 2BIT DEMIBOY Created by Space Sandwich
0x252525, 0x252525, 0x4B564D, 0x4B564D, 0x9AA57C, 0x9AA57C, 0xE0E9C4, 0xE0E9C4 },
{ // パレット2 TWILIGHT 5 Created by Star
0x292831, 0x292831, 0x333F58, 0x333F58, 0x4A7A96, 0xEE8695, 0xEE8695, 0xFBBBAD },
{ // パレット3 BLUE SKY 5 Created by Yuikawa-Akira
0x0D76D9, 0x3CADEC, 0x3CADEC, 0x71CDFC, 0x71CDFC, 0xBCE2FD, 0xBCE2FD, 0xFFFFFF },
{ // パレット4 MUMBLEBEE PALETTE Created by sudo_whoami
0x334455, 0x334455, 0x334455, 0x334455, 0xFFBB33, 0xFFBB33, 0xFFBB33, 0xFFBB33 },
{ // パレット5 DREAM HAZE 8 Created by Klafooty
0x3C42C4, 0x6E51C8, 0xA065CD, 0xCE79D2, 0xD68FB8, 0xDDA2A3, 0xEAC4AE, 0xF4DFBE },
{ // パレット6 DEEP MAZE Created by Ryosuke
0x001D2A, 0x085562, 0x009A98, 0x00BE91, 0x38D88E, 0x9AF089, 0xF2FF66, 0xF2FF66 },
{ // パレット7 NIGHT RAIN Created by Maber
0x000000, 0x012036, 0x3A7BAA, 0x7D8FAE, 0xA1B4C1, 0xF0B9B9, 0xFFD159, 0xFFFFFF },
};
uint32_t ColorPalettes8[8] = {
0x000000, 0x0000FF, 0x00FF00, 0x00FFFF, 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF
};
const size_t FixedPaletteSize = 8;
// 4x4 Bayerマトリクス (値域 0〜15)
static const uint8_t BAYER4x4[4][4] = {
{ 0, 8, 2, 10 },
{ 12, 4, 14, 6 },
{ 3, 11, 1, 9 },
{ 15, 7, 13, 5 }
};
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,
};
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 > maxPaletteIndex) {
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 loadColorPalettes8FromSD() {
const char* filename = "/ColorPalette8.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) {
uint32_t colorValue = strtoul(line.c_str(), NULL, 0);
ColorPalettes8[colorCount] = colorValue;
colorCount++;
}
}
file.close();
// 8色読み込めたか確認
if (colorCount == 8) {
return true; // 成功
} else {
// もし8色以下の場合は読み込めた分だけ反映して残りはデフォルトを使用する
return false; // 読み込み失敗(色が足りない)
}
}
bool loadDigital8ParamsFromSD() {
const char* path = "/Digital8.txt";
if (!SD.exists(path)) {
return false; // ファイルが存在しない場合はデフォルトを使うのでfalseを返す
}
File file = SD.open(path, FILE_READ);
if (!file) {
return false; // ファイルオープン失敗
}
int appliedCount = 0;
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() == 0 || line.startsWith("#")) {
continue; // 空行・コメント行はスキップ
}
int eqIndex = line.indexOf('=');
if (eqIndex <= 0) {
continue; // "=" が無い、または先頭にある不正な行はスキップ
}
String key = line.substring(0, eqIndex);
String value = line.substring(eqIndex + 1);
key.trim();
value.trim();
if (key == "sat_factor_x128") {
d8_satFactorX128 = (uint16_t)value.toInt();
appliedCount++;
} else if (key == "sigmoid_strength") {
d8_sigmoidStrength = value.toFloat();
appliedCount++;
} else if (key == "sigmoid_midpoint") {
d8_sigmoidMidpoint = value.toFloat();
appliedCount++;
} else if (key == "bias_width") {
d8_biasWidth = (int16_t)value.toInt();
appliedCount++;
}
// 未知のキーは無視 (将来の拡張やコメント代わりの記述を許容)
}
file.close();
return (appliedCount > 0); // 1つでも反映できればtrue
}
bool loadDither8ParamsFromSD() {
const char* path = "/Dither8.txt";
if (!SD.exists(path)) {
return false; // ファイルが存在しない場合はデフォルトを使うのでfalseを返す
}
File file = SD.open(path, FILE_READ);
if (!file) {
return false; // ファイルオープン失敗
}
int appliedCount = 0;
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() == 0 || line.startsWith("#")) {
continue; // 空行・コメント行はスキップ
}
int eqIndex = line.indexOf('=');
if (eqIndex <= 0) {
continue; // "=" が無い、または先頭にある不正な行はスキップ
}
String key = line.substring(0, eqIndex);
String value = line.substring(eqIndex + 1);
key.trim();
value.trim();
value.toLowerCase();
if (key == "dithering_levels") {
int loaded = value.toInt();
if (loaded < 2) loaded = 2; // 0除算防止の下限
if (loaded > 16) loaded = 16; // Bayer4x4の階調上限に合わせてクランプ
ditheringLevels = loaded;
appliedCount++;
} else if (key == "dithering_enabled") {
enableDithering = (value == "true" || value == "1");
appliedCount++;
}
// 未知のキーは無視 (将来の拡張やコメント代わりの記述を許容)
}
file.close();
return (appliedCount > 0); // 1つでも反映できればtrue
}
void makeDir() {
sprintf(foldername, "/%04d", folderCounter);
SD.mkdir(foldername);
}
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, 0); // 左右反転 0無効 1有効 ひっくり返して使う場合は1
s->set_vflip(s, 0); // 上下反転 0無効 1有効 ひっくり返して使う場合は1
s->set_saturation(s, 0); // 彩度 (-2〜2)
s->set_ae_level(s, 0); // 自動露出 (-2〜2)
s->set_contrast(s, 0); // コントラスト (-2〜2) 0を推奨
s->set_special_effect(s, 0); // ノーマル/ネガ/モノクロ (0〜6)(ただし3〜6は未対応)
s->set_wb_mode(s, 0); // オート/晴天/曇天/電球/蛍光灯 (0〜4) オートを推奨
//s->set_colorbar(s, 1); // カラーバー 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;
}
uint16_t swap16(uint16_t value) {
return (value << 8) | (value >> 8);
}
void applyDitherAndPalette_dither8(M5Canvas& srcSprite, M5Canvas& dstSprite, int levelsPerChannel, int paletteIndex, bool useDither) {
// =======================================================
// 1. 定数定義と初期化
// =======================================================
static const float BAYER_DENOMINATOR = 16.0f;
const int width = srcSprite.width();
const int height = srcSprite.height();
const int total_pixels = width * height;
// パレットインデックスの範囲チェックとポインタ設定
if (paletteIndex < 0 || paletteIndex > maxPaletteIndex) {
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. Ditherなしの場合 (スキップパス)
// =======================================================
if (!useDither) {
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. LUTディザ処理
// =======================================================
const uint32_t intervals = levelsPerChannel - 1;
// マイコンの処理を極限まで軽くするため、事前に計算結果をテーブル(LUT)化します。
// スタックメモリを約768バイト消費しますが、速度は飛躍的に向上します。
uint8_t base_level[256];
uint16_t error_scaled_16[256];
uint8_t quantized_values[256]; // levelsPerChannelの最大値は256を想定
// 出力値のLUT生成 (浮動小数点時の std::roundf(level_low) 相当)
for (int i = 0; i <= intervals; ++i) {
// 整数演算による四捨五入: (値 + 割る数/2) / 割る数
quantized_values[i] = (uint8_t)((i * 255 + intervals / 2) / intervals);
}
// 入力値(0〜255)ごとのベース階調と、比較用誤差のLUT生成
for (int i = 0; i < 256; ++i) {
uint32_t val_scaled = i * intervals;
// 整数除算で std::floorf 相当の切り捨て
uint32_t l_idx = val_scaled / 255;
if (l_idx >= intervals) {
l_idx = intervals - 1;
}
base_level[i] = l_idx;
// 誤差 (val_src - level_low) に intervals と 16 を掛けたスケールアップ値
error_scaled_16[i] = (uint16_t)((val_scaled - l_idx * 255) * 16);
}
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;
// bayerValue(0〜15) * 255 を整数比較用の閾値とする
// (浮動小数点時の bayerValue * step 相当)
const uint16_t bayer_thresh = BAYER4x4[y_idx][x_idx] * 255;
// 【読み込みとRGB成分抽出】
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];
// 【ディザリング適用 (LUTによる超高速化)】
for (int ch = 0; ch < 3; ++ch) {
uint8_t val_src = channels_src[ch];
// LUTからベースとなる階調インデックスを取得
uint32_t l_idx = base_level[val_src];
// スケールアップされた誤差と閾値を整数比較
if (error_scaled_16[val_src] >= bayer_thresh) {
l_idx++;
}
// LUTから量子化後の出力値を取得
channels_dithered[ch] = quantized_values[l_idx];
}
// 【パレット変換】ディザリング後の色を使って輝度を計算
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++;
}
}
}
// 1行ずつ輝度を抽出し、一気に複数パレットへ展開する超高速バッチ処理関数
void generatePaletteImages_LineByLine(M5Canvas& srcSprite, M5Canvas* dstSprites, const uint32_t** palettes, int numPalettes, int levelsPerChannel, bool useDither) {
const int width = srcSprite.width();
const int height = srcSprite.height();
// 1行分の階調インデックス(0〜7)を保存するバッファ
uint8_t* gray_buffer = (uint8_t*)malloc(width);
if (!gray_buffer) {
Serial.println("Memory allocation failed");
return;
}
// ==========================================
// 1. LUT初期化 (ディザリングありの場合のみ)
// ==========================================
const uint32_t intervals = levelsPerChannel - 1;
uint8_t base_level[256];
uint16_t error_scaled_16[256];
uint8_t quantized_values[256];
if (useDither && intervals > 0) {
for (int i = 0; i <= intervals; ++i) {
quantized_values[i] = (uint8_t)((i * 255 + intervals / 2) / intervals);
}
for (int i = 0; i < 256; ++i) {
uint32_t val_scaled = i * intervals;
uint32_t l_idx = val_scaled / 255;
if (l_idx >= intervals) l_idx = intervals - 1;
base_level[i] = l_idx;
error_scaled_16[i] = (uint16_t)((val_scaled - l_idx * 255) * 16);
}
}
uint16_t* src_buffer_base = (uint16_t*)srcSprite.getBuffer();
// ==========================================
// 2. メインループ (行単位での処理)
// ==========================================
for (int y = 0; y < height; ++y) {
const int y_idx = y & 3;
uint16_t* src_line_ptr = src_buffer_base + y * width;
// ------------------------------------------------
// ステージA: 1行分の輝度(階調)を計算
// ------------------------------------------------
for (int x = 0; x < width; ++x) {
uint16_t rgb565 = swap16(src_line_ptr[x]);
// RGB565 -> 888展開
uint8_t r = (rgb565 >> 11) << 3;
r |= r >> 5;
uint8_t g = ((rgb565 >> 5) & 0x3F) << 2;
g |= g >> 6;
uint8_t b = (rgb565 & 0x1F) << 3;
b |= b >> 5;
// ディザリング適用
if (useDither && intervals > 0) {
const int x_idx = x & 3;
const uint16_t bayer_thresh = BAYER4x4[y_idx][x_idx] * 255;
uint8_t channels[3] = { r, g, b };
for (int ch = 0; ch < 3; ++ch) {
uint8_t val = channels[ch];
uint32_t l_idx = base_level[val];
if (error_scaled_16[val] >= bayer_thresh) {
l_idx++;
}
channels[ch] = quantized_values[l_idx];
}
r = channels[0];
g = channels[1];
b = channels[2];
}
// 輝度を計算して8階調(0-7)インデックスに直結
uint32_t luminance = (54U * r + 183U * g + 19U * b) >> 8;
uint8_t grayLevel = luminance >> 5;
if (grayLevel > 7) grayLevel = 7;
// 結果を「パレットインデックス」として保存
gray_buffer[x] = grayLevel;
}
// ------------------------------------------------
// ステージB: パレットルックアップと書き込み (8枚分)
// ------------------------------------------------
for (int p = 0; p < numPalettes; ++p) {
// 該当パレットの該当行のバッファ位置を計算
uint8_t* dst_line_ptr = (uint8_t*)dstSprites[p].getBuffer() + y * width * 3;
const uint32_t* currentPalette = palettes[p];
for (int x = 0; x < width; ++x) {
// ステージAで計算済みの階調(0-7)を読み出して色に変換するだけ
uint32_t color = currentPalette[gray_buffer[x]];
*dst_line_ptr++ = (color >> 16) & 0xFF; // R
*dst_line_ptr++ = (color >> 8) & 0xFF; // G
*dst_line_ptr++ = color & 0xFF; // B
}
}
}
// メモリ解放
free(gray_buffer);
}
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;
}
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 = ColorPalettes8[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 applyContrastLUT(uint8_t& r, uint8_t& g, uint8_t& b) {
r = contrastLUT[r];
g = contrastLUT[g];
b = contrastLUT[b];
}
void initSigmoidContrastLUT(float strength = 8.0f, float midpoint = 0.5f) {
for (int i = 0; i < 256; i++) {
float x = i / 255.0f;
// コントラスト低下 = sigmoid曲線の逆関数
// f^-1(x) = midpoint - ln(1/y - 1) / strength
// ただし端点正規化のため y を [sig_0, sig_1] 空間に写してから逆変換する
float sig_0 = 1.0f / (1.0f + expf(-strength * (0.0f - midpoint)));
float sig_1 = 1.0f / (1.0f + expf(-strength * (1.0f - midpoint)));
// x を [sig_0, sig_1] 空間に写す
float y = x * (sig_1 - sig_0) + sig_0;
y = constrain(y, 0.0001f, 0.9999f); // log保護
// 逆sigmoid
float result = midpoint - logf(1.0f / y - 1.0f) / strength;
contrastLUT[i] = (uint8_t)constrain(result * 255.0f + 0.5f, 0, 255);
}
}
void applyModulate_integer(uint8_t& r, uint8_t& g, uint8_t& b, uint16_t sat_factor_x128 = 256) {
uint8_t cmax = std::max({ r, g, b });
uint8_t cmin = std::min({ r, g, b });
uint8_t delta = cmax - cmin;
if (delta == 0 || cmax == 0) return; // 無彩色 or 黒 → 変化なし
uint16_t new_chroma = (uint16_t)std::min(
(uint32_t)delta * sat_factor_x128 / 128,
(uint32_t)cmax);
uint8_t m = cmax - (uint8_t)new_chroma;
if (cmax == r) {
if (g >= b) {
r = cmax;
g = m + (uint8_t)((uint32_t)new_chroma * (g - cmin) / delta);
b = m;
} else {
r = cmax;
g = m;
b = m + (uint8_t)((uint32_t)new_chroma * (b - cmin) / delta);
}
} else if (cmax == g) {
if (r >= b) {
r = m + (uint8_t)((uint32_t)new_chroma * (r - cmin) / delta);
g = cmax;
b = m;
} else {
r = m;
g = cmax;
b = m + (uint8_t)((uint32_t)new_chroma * (b - cmin) / delta);
}
} else {
if (g >= r) {
r = m;
g = m + (uint8_t)((uint32_t)new_chroma * (g - cmin) / delta);
b = cmax;
} else {
r = m + (uint8_t)((uint32_t)new_chroma * (r - cmin) / delta);
g = m;
b = cmax;
}
}
}
void applyDitherAndPalette_digital8(M5Canvas& srcSprite, M5Canvas& dstSprite) {
const int width = srcSprite.width();
const int height = srcSprite.height();
uint16_t* src_buffer = (uint16_t*)srcSprite.getBuffer();
uint8_t* dst_buffer = (uint8_t*)dstSprite.getBuffer();
if (!src_buffer || !dst_buffer) {
Serial.println("Error: Canvas buffer not accessible.");
errorLed();
return;
}
for (int y = 0; y < height; ++y) {
const int y_idx = y & 3;
uint16_t* src_row = src_buffer + (ptrdiff_t)y * width;
uint8_t* dst_row = dst_buffer + (ptrdiff_t)y * width * 3;
for (int x = 0; x < width; ++x) {
const int x_idx = x & 3;
// --- ステージ0: RGB565 → RGB888 ---
uint16_t rgb565 = swap16(src_row[x]);
uint8_t r = (rgb565 >> 11) << 3;
r |= r >> 5;
uint8_t g = ((rgb565 >> 5) & 0x3F) << 2;
g |= g >> 6;
uint8_t b = (rgb565 & 0x1F) << 3;
b |= b >> 5;
// --- ステージ1: 彩度アップ ---
applyModulate_integer(r, g, b, d8_satFactorX128); // 128 → 1.0x, 256 → 2.0x
// --- ステージ2: 逆シグモイド関数 ---
r = contrastLUT[r];
g = contrastLUT[g];
b = contrastLUT[b];
// --- ステージ3: ディザリング処理 ---
uint8_t bv = BAYER4x4[y_idx][x_idx];
int16_t bayer_bias = (int16_t)(((int32_t)bv * (d8_biasWidth * 2)) >> 4) - d8_biasWidth;
// バイアス加算
int16_t r_biased = (int16_t)r + bayer_bias;
int16_t g_biased = (int16_t)g + bayer_bias;
int16_t b_biased = (int16_t)b + bayer_bias;
auto quantize8 = [](int16_t val) -> uint8_t {
// 0〜255にクランプ
if (val < 0) val = 0;
if (val > 255) val = 255;
// 8段階(0〜7)に量子化: val*7/255 を四捨五入
uint8_t step = (uint8_t)((val * 7 + 127) / 255);
// 255スケールに復元: step*255/7
return (uint8_t)(step * 255 / 7);
};
uint8_t r_temp = quantize8(r_biased);
uint8_t g_temp = quantize8(g_biased);
uint8_t b_temp = quantize8(b_biased);
// --- ステージ4: 最近傍パレットマッピング ---
uint8_t r_out, g_out, b_out;
quantize_color_to_closest(r_temp, g_temp, b_temp, &r_out, &g_out, &b_out);
dst_row[x * 3 + 0] = r_out;
dst_row[x * 3 + 1] = g_out;
dst_row[x * 3 + 2] = b_out;
}
}
}
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;
}
// 処理開始時に一度だけ実行される関数
bool initializeProcess() {
Serial.println(">>> Process STARTED! <<<");
if (!SD.begin(15, SPI, 10000000)) {
Serial.println("ERROR: SD Card mount failed.");
errorLed();
return false;
}
return true;
}
// NORMAL撮影
void executeSingleProcess() {
if (!CameraGet()) {
Serial.println("ERROR: Camera capture failed (Single).");
errorLed();
return;
}
canvas_565_p.pushImage(0, 0, PICTURE_WIDTH_PIX, PICTURE_HEIGHT_PIX, (uint16_t*)fb->buf);
CameraFree();
// パレットのポインタ配列を準備
const uint32_t* paletteArray[8];
for (int i = 0; i < 8; i++) {
paletteArray[i] = ColorPalettes[i];
}
// ==========================================
// 1. 8パレット一括生成 (ディザリングの有無は enableDithering に従う)
// ==========================================
generatePaletteImages_LineByLine(canvas_565_p, dstSprites, paletteArray, 8, ditheringLevels, enableDithering);
for (int i = 0; i <= maxPaletteIndex; i++) {
sprintf(filename, "/%04d_d%02d.png", folderCounter, i);
//display.drawCenterString(filename, display.width() / 2, display.height() / 2);
savePNGToSD(dstSprites[i], filename);
}
// ==========================================
// 2. digital8 (専用処理)
// ==========================================
applyDitherAndPalette_digital8(canvas_565_p, dstSprites[0]);
sprintf(filename, "/%04d_d08.png", folderCounter);
//display.drawCenterString(filename, display.width() / 2, display.height() / 2);
savePNGToSD(dstSprites[0], filename);
// ==========================================
// 3. raw画像保存
// ==========================================
sprintf(filename, "/%04d_raw.png", folderCounter);
//display.drawCenterString(filename, display.width() / 2, display.height() / 2);
savePNGToSD(canvas_565_p, filename);
currentPaletteIndex = 0;
}
// 処理停止時に一度だけ実行される関数
void cleanupProcess() {
Serial.println(">>> Process STOPPED! <<<");
folderCounter = (folderCounter + 1) % 10000;
writefolderCounter_internal(folderCounter);
SD.end();
fileCounter = 0;
//neopixelWrite(LED_PIN, 0, 0, 0);
}
void errorLed() {
neopixelWrite(LED_PIN, 200, 0, 0); // Red
}
void setup() {
Serial.begin(115200);
pinMode(POWER_GPIO_NUM, OUTPUT);
digitalWrite(POWER_GPIO_NUM, LOW);
//delay(500);
neopixelWrite(LED_PIN, 0, 0, 200);
pinMode(KEY_PIN, INPUT_PULLUP);
Serial.println("Allocating 8 Canvases in PSRAM...");
for (int i = 0; i < NUM_PALETTES; ++i) {
dstSprites[i].setPsram(true); // 描画先を必ずPSRAMに強制する
dstSprites[i].setColorDepth(24); // RGB888 (24bit) フォーマットを指定
// Canvasの生成 (内部でPSRAMにメモリが確保される)
if (dstSprites[i].createSprite(PICTURE_WIDTH_PIX, PICTURE_HEIGHT_PIX) == nullptr) {
Serial.printf("Error: Failed to create Canvas %d. Check PSRAM.\n", i);
errorLed();
while (1) { delay(100); }
}
}
Serial.println("Canvas allocation successful.");
canvas_565_p.setColorDepth(16);
canvas_565_p.createSprite(PICTURE_WIDTH_PIX, PICTURE_HEIGHT_PIX);
// 一度SDカードをマウントして確認
SPI.begin(7, 8, 6, -1);
if (!SD.begin(15, SPI, 10000000)) {
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 <= maxPaletteIndex; 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(10);
}
// digital8処理用の固定8色パレットの読み込み (無ければ現状のデフォルト値のまま)
if (loadColorPalettes8FromSD()) {
Serial.println("Palette 8 loaded from SD.");
//display.println("Palette 8 from SD");
} else {
Serial.println("Palette 8 use default.");
//display.println("Palette 8 default");
}
delay(10);
// 8-Color Ditherモードの階調数読み込み (無ければ現状のデフォルト値のまま)
if (loadDither8ParamsFromSD()) {
Serial.println("Dither params loaded from SD.");
//display.println("Dither from SD");
} else {
Serial.println("Dither params use default.");
//display.println("Dither default");
}
delay(10);
// digital8処理パラメータの読み込み (無ければ現状のデフォルト値のまま)
if (loadDigital8ParamsFromSD()) {
Serial.println("Digital 8 params loaded from SD.");
//display.println("Digital 8 from SD");
} else {
Serial.println("Digital 8 params use default.");
//display.println("Digital 8 default");
}
delay(10);
}
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!");
initSigmoidContrastLUT(d8_sigmoidStrength, d8_sigmoidMidpoint);
//delay(500);
//display.clear(TFT_BLACK);
neopixelWrite(LED_PIN, 0, 200, 200);
}
void loop() {
if (!digitalRead(KEY_PIN)) {
if (!initializeProcess()) {
return; // SDマウント失敗なら撮影処理に進まない
}
keyOnTime = millis(); // 最後にkey操作した時間
Serial.println("--- Start Recording (NORMAL) ---");
neopixelWrite(LED_PIN, 200, 200, 0);
executeSingleProcess();
cleanupProcess();
neopixelWrite(LED_PIN, 0, 200, 200);
Serial.println("--- Stop Recording (NORMAL) ---");
}
//一定時間操作していないとLEDをOFF 平均消費電流が45mAを下回るとTailBATが40秒後に自動OFFする
if ((millis() - keyOnTime >= LED_ON_DURATION)) {
neopixelWrite(LED_PIN, 0, 0, 0);
}
}
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
*/
@hmax42

hmax42 commented Mar 27, 2025

Copy link
Copy Markdown

ESP-ROM:esp32s3-20210327
16:29:10.883 -> Build:Mar 27 2021
16:29:10.883 -> rst:0xc (RTC_SW_CPU_RST),boot:0x1a (SPI_FAST_FLASH_BOOT)
16:29:10.883 -> Saved PC:0x4204a476
16:29:10.883 -> SPIWP:0xee
16:29:10.883 -> mode:DIO, clock div:1
16:29:10.883 -> load:0x3fce3808,len:0x3ac
16:29:10.883 -> load:0x403c9700,len:0x9a8
16:29:10.883 -> load:0x403cc700,len:0x2920
16:29:10.883 -> entry 0x403c98b8
16:29:11.023 -> E (141) psram: PSRAM ID read error: 0x00ffffff, PSRAM chip not found or not supported, or wrong PSRAM line mode
16:29:12.796 -> E (1948) cam_hal: cam_dma_config(300): frame buffer malloc failed
16:29:12.796 -> E (1950) cam_hal: cam_config(384): cam_dma_config failed
16:29:12.842 -> E (1950) camera: Camera config failed with error 0xffffffff

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

@Yuikawa-Akira

Copy link
Copy Markdown
Author

My Arduino IDE setup is this.
2025-03-28_020611

@hmax42

hmax42 commented Mar 27, 2025

Copy link
Copy Markdown

thanks,
i had outdated M5Unified and M5Gfx libraries
works now
nice work on the sketch

@iNu0805

iNu0805 commented Jul 11, 2025

Copy link
Copy Markdown

Arduinoで書き込みを行った結果
Compilation error: 'esp_spiram_get_size' was not declared in this scope
というエラーメッセージが出てしまいました。

ボードはM5AtomS3を認識しておりesp32 by Espressif Systemsは3.2.1をインストールしています。
プログラムの知識がなく、解決策がわからないので良ければお教えください

@Yuikawa-Akira

Copy link
Copy Markdown
Author

ボードの設定を以下のようにしてみてください(出先なので今環境がなく……)
427677485-538dee46-1b4f-4884-a47b-d6ae5e9d076a

@iNu0805

iNu0805 commented Jul 12, 2025

Copy link
Copy Markdown

ボードの設定を上記のように行ったのですが、M5AtomS3を認識してしまっておりM5AtomS3Rとは認識してくれません

@Yuikawa-Akira

Copy link
Copy Markdown
Author

手動で一番上のボード設定をM5AtomS3Rに変えてコンパイルしてみてください

@iNu0805

iNu0805 commented Jul 12, 2025

Copy link
Copy Markdown

ボード設定を変えて、ボードを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
というエラーメッセージが出てしまいます

@Yuikawa-Akira

Yuikawa-Akira commented Jul 13, 2025

Copy link
Copy Markdown
Author

size_t psram_size = esp_spiram_get_size() / 1048576;の行をコメントアウトしてみてください

//size_t psram_size = esp_spiram_get_size() / 1048576;

@iNu0805

iNu0805 commented Jul 13, 2025

Copy link
Copy Markdown

できました!!
本当にありがとうございます!!!!!

@kmbrhrt

kmbrhrt commented Aug 20, 2025

Copy link
Copy Markdown

書き込む際に以下のようなエラーが出てしまいます、何が原因でしょうか。
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

@Yuikawa-Akira

Copy link
Copy Markdown
Author

内蔵の緑色のLEDが点灯するまで側面のリセットボタンを長押し(約2秒)してから離すとダウンロードモードになり、書き込み待機状態になるので試してみてください。
それでもダメならばCOMポートの番号が違うかもしれないので別のポート番号を選んでみてください。

@kmbrhrt

kmbrhrt commented Aug 20, 2025

Copy link
Copy Markdown

ダウンロードモードにして、シリアルポートをjdev/cu.usbmodem21101にすることで解決しました!ありがとうございます!
たくさん写真撮ってきます!

@Yuikawa-Akira

Copy link
Copy Markdown
Author

このGistは以下のリポジトリに移行しました
This Gist has been moved to the following repository.
https://github.com/Yuikawa-Akira/Pict_Camera

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment