編集履歴一覧に戻る
eMoi-picのアイコン画像

eMoi-pic が 2024年06月17日15時37分38秒 に編集

初版

タイトルの変更

+

EMOI-PIC 連続撮影+9枚セレクション

タグの変更

+

SPRESENSE

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

セットアップや使用方法

本文の変更

+

# EMOI-PIC 画像評価部サンプルコード - Spresenseカメラで連続撮影する画像の「エモさ」を評価し、その評価値に基づき、指定枚数(9枚)分だけSDカードに保存するサンプルコードです。 - 評価値の例として、本コードでは「中心部のUV値の分散 - 周辺部のUV値の分散」としています。意味的には、画像の周辺部より中心部により彩度の高い対象がある画像を「エモい」とみなすことになります。 - このコードを基本に、各自の画像評価値を定義し、ロボット制御部を追加すると最終コードとなります。 ![](https://camo.elchika.com/85c7dedd26d04933e3841705f093145532e490a5/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f64343830333136392d363131652d343365322d383663332d6331616438303838646561382f39663839333033372d356239392d343439372d616530362d396366323264663566653238/) ``` // Spresense EMOI-PIC sample code // // 2024/6/17: 外周部より中央部の方がUVの分散が高い画像を「エモい」と定義し、それを指定数枚撮影する。指定数に達した以降は、最も低い評価度の画像を削除し、最新のものを追加する。 #include <stdio.h> #include <Camera.h> #include <Adafruit_ILI9341.h> #include <SDHCI.h> #define TFT_DC (9) #define TFT_CS (10) // ディスプレイの縦横の大きさ #define DISPLAY_WIDTH (320) #define DISPLAY_HEIGHT (240) // CAM_IMGSIZE_VGA_H(640), CAM_IMGSIZE_VGA_V(480)の1/4 #define IMAGE_X_SIZE (160) #define IMAGE_Y_SIZE (120) #define PERIPHERALl_X_WIDTH (40) #define PERIPHERALl_Y_WIDTH (30) #define TOTAL_EMOIC_PICTURE_COUNT (9) // SDカードの保存する写真の枚数を指定します #define TOTAL_PICTURE_COUNT (36) // 36回撮影したら中断し終了します SDClass theSD; Adafruit_ILI9341 display = Adafruit_ILI9341(TFT_CS, TFT_DC); uint8_t R[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] uint8_t G[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] uint8_t B[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] uint8_t Y[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] int8_t U[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [-128-0-127] int8_t V[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [-128-0-127] uint8_t buf_R[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] uint8_t buf_G[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] uint8_t buf_B[CAM_IMGSIZE_QQVGA_H][CAM_IMGSIZE_QQVGA_V]; // 160*120, [0-255] uint16_t drawImageBuf[CAM_IMGSIZE_QQVGA_H*CAM_IMGSIZE_QQVGA_V]; int take_picture_count = 0; int store_picture_count = 0; int picture_eval_score = 0; int16_t min_eval_score = 0xffff; int file_exist[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; int eval_table[9] = {-1, -1, -1, -1, -1, -1, -1, -1, -1}; // 320*240液晶ディスプレイの下半分に指定行(0-5)の高さに文字列を指定色で左右中央に1行表示する。 // ILI9341_BLACK (_RED, _GREEN, _BLUE, _WHITE etc.) // コードを変更しないでください。 void putStringOnLCD(String str, int line, int color) { int len = str.length(); display.fillRect(0, DISPLAY_HEIGHT/2+line*18+6, DISPLAY_WIDTH-1, DISPLAY_HEIGHT/2+(line+1)*18+5, ILI9341_BLACK); display.setTextSize(2); int sx = DISPLAY_WIDTH/2 - len/2*12; if (sx < 0) sx = 0; if (line < 0) sx = 0; else if (line > 5) sx = 5; display.setCursor(sx, DISPLAY_HEIGHT/2+line*18+8); display.setTextColor(color); display.println(str); } // 静止画(640*480)を1枚撮影し、SDカードにJPEG形式で保存する。 // コードを変更しないでください。 void takeStillPicture_SD(int num) { putStringOnLCD("Taking Still Picture", 5, ILI9341_WHITE); CamImage still_img = theCamera.takePicture(); if (still_img.isAvailable()) { char filename[16] = {0}; sprintf(filename, "PICT%03d.JPG", num); theSD.remove(filename); File myFile = theSD.open(filename, FILE_WRITE); myFile.write(still_img.getImgBuff(), still_img.getImgSize()); myFile.close(); putStringOnLCD(filename, 4, ILI9341_WHITE); putStringOnLCD("SD card saved", 5, ILI9341_WHITE); } else { putStringOnLCD("ERROR: Take Still Picture", 5, ILI9341_RED); } } // RGB565の3配列(buf_R, buf_G, buf_B, 160*120)をカラー画像としてディスプレイ右に表示する。 // コードを変更しないでください。 void displayImage() { for (int n = 0; n < IMAGE_X_SIZE*IMAGE_Y_SIZE; n++) { uint8_t r5 = buf_R[(int)(n%CAM_IMGSIZE_QQVGA_H)][n/CAM_IMGSIZE_QQVGA_H]; uint8_t g6 = buf_G[(int)(n%CAM_IMGSIZE_QQVGA_H)][n/CAM_IMGSIZE_QQVGA_H]; uint8_t b5 = buf_B[(int)(n%CAM_IMGSIZE_QQVGA_H)][n/CAM_IMGSIZE_QQVGA_H]; drawImageBuf[n] = (uint16_t)(((uint16_t)r5 & 0x00f8)<<8) | (((uint16_t)g6 & 0x00fc)<<3) | (((uint16_t)b5 & 0x00f8)>>3); } display.drawRGBBitmap(DISPLAY_WIDTH/2, 0, drawImageBuf, CAM_IMGSIZE_QQVGA_H, CAM_IMGSIZE_QQVGA_V); } // Spresenseカメラライブラリのコールバック関数。撮影フレーム毎に入力カラー画像をYUVの3配列(160*120)に格納する。 // ユーザはこの中に画像を評価するコードを記述し、得られた評価値をグローバル変数picture_eval_scoreに書き込む。 void CamCB(CamImage img) { if (!img.isAvailable()) return; display.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), CAM_IMGSIZE_QQVGA_H, CAM_IMGSIZE_QQVGA_V); uint16_t* imgbuf_RGB565 = (uint16_t*)img.getImgBuff(); int n = 0; for (int y = 0; y < IMAGE_Y_SIZE; y++) { for(int x = 0; x < IMAGE_X_SIZE ; x++) { uint16_t val = imgbuf_RGB565[n++]; R[x][y] = (val>>8) & 0x00f8; // 11111000 G[x][y] = (val>>3) & 0x00fc; // 11111100 B[x][y] = (val<<3) & 0x00f8; // 11111000 } } // RGBからYCbCr(YUV)に変換し、各チャンネル毎に8btにスケーリングして格納。YはR+2G+Bで近似 for (int x = 0; x < IMAGE_X_SIZE; x++) { for(int y = 0; y < IMAGE_Y_SIZE ; y++) { int tY = ((int)R[x][y]+(int)G[x][y]*2+(int)B[x][y]) / 4; Y[x][y] = tY; // [0-255] int val = (int)B[x][y] - tY; if(val > 127) U[x][y] = 127; else if(val < -128) U[x][y] = -128; else U[x][y] = val; val = (int)R[x][y] - tY; if(val > 127) V[x][y] = 127; else if(val < -128) V[x][y] = -128; else V[x][y] = val; } } //////// USER CODE ///////// // R[X座標(0-159)][Y座標(0-119] 値域[0-255] // G[X座標(0-159)][Y座標(0-119] 値域[0-255] // B[X座標(0-159)][Y座標(0-119] 値域[0-255] // Y[X座標(0-159)][Y座標(0-119] 値域[0-255] // U[X座標(0-159)][Y座標(0-119] 値域[-128-0-127] // V[X座標(0-159)][Y座標(0-119] 値域[-128-0-127] // 画像左上が原点で、Xは右方向、Yは下方向 /////////////// ここから //////////////// // 外周部のUVの分散を算出 int var_pU = 0; int var_pV = 0; for(int x = 0; x < PERIPHERALl_X_WIDTH; x++) { for(int y = 0; y < IMAGE_Y_SIZE; y++) { if(R[x][y]==0 || G[x][y]==0 || B[x][y]==0|| R[x][y]>=240 || G[x][y]>=248 || B[x][y]>=240) break; // RGBの各値が上限下限で飽和していたらスキップ var_pU += U[x][y]^2; var_pV += V[x][y]^2; n++; } } for(int x = IMAGE_X_SIZE - PERIPHERALl_X_WIDTH; x < IMAGE_X_SIZE; x++) { for(int y = 0; y < IMAGE_Y_SIZE; y++) { if(R[x][y]==0 || G[x][y]==0 || B[x][y]==0|| R[x][y]>=240 || G[x][y]>=248 || B[x][y]>=240) break; // RGBの各値が上限下限で飽和していたらスキップ var_pU += U[x][y]^2; var_pV += V[x][y]^2; n++; } } for(int x = PERIPHERALl_X_WIDTH; x < IMAGE_X_SIZE - PERIPHERALl_X_WIDTH; x++) { for(int y = PERIPHERALl_Y_WIDTH; y < IMAGE_Y_SIZE - PERIPHERALl_Y_WIDTH; y++) { if(R[x][y]==0 || G[x][y]==0 || B[x][y]==0|| R[x][y]>=240 || G[x][y]>=248 || B[x][y]>=240) break; // RGBの各値が上限下限で飽和していたらスキップ var_pU += U[x][y]^2; var_pV += V[x][y]^2; n++; } } for(int x = PERIPHERALl_X_WIDTH; x < IMAGE_X_SIZE - PERIPHERALl_X_WIDTH; x++) { for(int y = IMAGE_Y_SIZE - PERIPHERALl_Y_WIDTH; y < IMAGE_Y_SIZE; y++) { if(R[x][y]==0 || G[x][y]==0 || B[x][y]==0|| R[x][y]>=240 || G[x][y]>=248 || B[x][y]>=240) break; // RGBの各値が上限下限で飽和していたらスキップ var_pU += U[x][y]^2; var_pV += V[x][y]^2; n++; } } if( n != 0) { var_pU /= n; var_pV /= n; } // 中心部のUVの分散を算出 int var_cU = 0; int var_cV= 0; n = 0; for(int x = PERIPHERALl_X_WIDTH; x < IMAGE_X_SIZE - PERIPHERALl_X_WIDTH; x++) { for(int y = PERIPHERALl_Y_WIDTH; y < IMAGE_Y_SIZE - PERIPHERALl_Y_WIDTH; y++) { if(R[x][y]==0 || G[x][y]==0 || B[x][y]==0|| R[x][y]>=240 || G[x][y]>=248 || B[x][y]>=240) break; // RGBの各値が上限下限で飽和していたらスキップ var_cU += U[x][y]^2; var_cV += V[x][y]^2; n++; } } if( n != 0) { var_cU /= n; var_cV /= n; } // 「中央部の分散-周辺部の分散」を画像評価値とし、グローバル変数のpicture_eval_scoreに保存する。 picture_eval_score = var_cU + var_cV - var_pU - var_pU; char message[32] = {0}; sprintf(message, "PERIPHERAL %d", var_pU+var_pV); putStringOnLCD(message, 0, ILI9341_WHITE); sprintf(message, "CENTER %d", var_cU+var_cV); putStringOnLCD(message, 1, ILI9341_WHITE); /////////////// ここまで //////////////// } // Spresenseとっディスプレイを初期化します。Spresenseカメラのプレビュー画像のフレームレートを5fps、160*120画素、YUV422形式に設定します。 // Spresenseカメラの静止画を640*480画素のJPEGに設定します。 // コードを変更しないでください。 void setup() { CamErr err; display.begin(); display.setRotation(3); display.fillScreen(ILI9341_BLACK); while (!theSD.begin()) { putStringOnLCD("Insert SD card", 0, ILI9341_GREEN); } display.fillScreen(ILI9341_BLACK); err = theCamera.begin(1, CAM_VIDEO_FPS_5, CAM_IMGSIZE_QQVGA_H, CAM_IMGSIZE_QQVGA_V, CAM_IMAGE_PIX_FMT_RGB565); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_AUTO); err = theCamera.setStillPictureImageFormat(CAM_IMGSIZE_VGA_H, CAM_IMGSIZE_VGA_V, CAM_IMAGE_PIX_FMT_JPG); err = theCamera.startStreaming(true, CamCB); char filename[16] = {0}; for(int i = 0; i < TOTAL_EMOIC_PICTURE_COUNT ; i++) { sprintf(filename, "PICT%03d.JPG", i); theSD.remove(filename); } } // ユーザはこの中に画像の評価値に基づいて静止画を撮影するか否かを判断するコードを記述します。 // takeStillPicture_SD()をコールすると、撮影画像1枚をSDカードに保存します。保存処理に1秒程度要することに留意してください。 void loop() { /////////////// ここから //////////////// int16_t min_score = 0x7fff; int min_index = -1; int n; delay(100); // ロボットの移動&停止動作 // 撮影枚数が規定数のTOTAL_PICTURE_COUNTに達したら終了 if (take_picture_count == TOTAL_PICTURE_COUNT) { putStringOnLCD("Picture Count END", 5, ILI9341_GREEN); theCamera.end(); exit(0); } // グローバル変数のpicture_eval_scoreの評価値が最高評価値より高い場合が静止画を1枚撮影し、SDカードにJPEG保存する。 if(picture_eval_score > min_eval_score) { if(store_picture_count < TOTAL_EMOIC_PICTURE_COUNT) { for(n = 0; n < TOTAL_EMOIC_PICTURE_COUNT ; n++) { if(file_exist[n] == 0) break; } takeStillPicture_SD(n); eval_table[n] = picture_eval_score; file_exist[n] = 1; take_picture_count++; store_picture_count++; } else { for(n=0; n < TOTAL_EMOIC_PICTURE_COUNT ; n++) { if(eval_table[n] < min_score) { min_score = eval_table[n]; min_index = n; } } takeStillPicture_SD(min_index); eval_table[min_index] = picture_eval_score; take_picture_count++; } } char message[32] = {0}; sprintf(message, "Take: %d Store: %d", take_picture_count, store_picture_count); putStringOnLCD(message, 2, ILI9341_GREEN); /////////////// ここまで //////////////// } ```