ESP32で円形POVを作るシリーズの最終回、目標であるPOVでSDカードの動画を流します。
動画ファイルの形式について
前回の記事で連続したBMPファイルを連続して読み込むことに成功しました。そのため、今回もBMPファイルをたくさん用意してそれをパラパラ漫画のように表示する方式を取ります。
POVの特徴として、回転数=リフレッシュレートということがあります。つまり回転数以上のフレームレート(fps)は出ないこととなり、例えば毎秒20回転の時は20fpsかそれ以下になります。じゃあ、fpsが回転数よりも下がるのはどういう場合かというと、SDカードから1枚の画像を読み込む時間が1/回転数(秒)に満たない場合になり、ここで画像のデータサイズとSDカードの読み込みスピードがどれくらいかがポイントになります。画像のサイズが大きい場合やSDカードの読み込みスピードが遅い場合は画像を圧縮することで両方のスピードを上げることができます。圧縮はRGB565かjpgの形式で保存して読み込み、復号化することになります。(が、今回はBMPで十分余裕があるためやりません)
前回の記事↓
動画を変換するPythonプログラム
2回目の記事で画像ファイルを円形POV用に変換するプログラムを載せましたが、今回は動画ファイル(mp4)を変換するプログラムです。実行すると動画ファイルと同じ名前のフォルダが作成され、その中に「000000.bmp」から始まる連番BMPが保存されます。プログラム実行時に表示されるFPS(フレームレート)とFrame Count(フレーム数)は後ほどのArduinoプログラムで必要なのでメモしておきます。また、2回目と同様、「ガンマ補正値」「LEDの数」「動画ファイルのフォルダアドレス」「動画ファイル名(「.mp4」は入れない)」は環境に合わせて書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# -*- coding: utf-8 -*- from PIL import Image import numpy as np import math import cv2 import os import sys #ガンマ補正値 gammaR = 6.0 gammaG = 6.0 gammaB = 6.0 #LEDの数を設定 LEDNum = 30 np.set_printoptions(threshold=np.inf)#printの表示を省略しない dirName = '動画があるフォルダのアドレス' fileName = '動画ファイル名' #フォルダ作製 os.makedirs(dirName+'\\'+fileName, exist_ok=True) # 元となる動画の読み込み cap = cv2.VideoCapture(dirName+'\\'+fileName+'.mp4') #動画情報の読み込み print('Width:',end='') frameWidth = cap.get(cv2.CAP_PROP_FRAME_WIDTH) print(frameWidth) print('Height:',end='') frameHeight = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) print(frameHeight) print('FPS:',end='') fps = cap.get(cv2.CAP_PROP_FPS) print(fps) print('Frame Count:',end='') frameCount = cap.get(cv2.CAP_PROP_FRAME_COUNT) print(frameCount) print('Time[s]:',end='') print(frameCount/fps) #出力画像の幅と高さを設定 width = LEDNum height = round(LEDNum*3*3.14*2/2) #リサイズ画像の幅と高さを設定 if frameWidth>frameHeight: #POV内に全て表示する場合 #reWidth = LEDNum*2 #reHeight = math.floor(reWidth*frameHeight/frameWidth) #POVいっぱいに表示する場合 reHeight = LEDNum*2 reWidth = math.floor(reHeight*frameWidth/frameHeight) else : #POV内に全て表示する場合 #reHeight = LEDNum*2 #reWidth = math.floor(reHeight*frameWidth/frameHeight) #POVいっぱいに表示する場合 reWidth = LEDNum*2 reHeight = math.floor(reWidth*frameHeight/frameWidth) print(reWidth, reHeight) #動画画像の出力 for fc in range(int(frameCount)): #動画のフレーム読み込み ret, frame = cap.read() #retは読み込めたかのbool値、frameはndarray[][][] if not ret: break #リサイズ frame_resize = cv2.resize(frame,dsize=(reWidth,reHeight)) img_pixels = np.zeros((height,width,3),dtype=np.uint8) for deg in range(height): for r in range(width): #画像中心から距離r,角度 2pi*deg/heightのピクセル位置を計算 x = math.floor(reWidth/2 + (r+0.5) * math.sin(2*math.pi * deg/height)) y = math.floor(reHeight/2 - (r+0.5) * math.cos(2*math.pi * deg/height)) # で左からx番目,上からy番目のピクセルの色を取得し、img_pixelsに追加する if (x<reWidth and x>=0) and (y<reHeight and y>=0): #print(x,y) #print(frame_resize.shape) img_pixels[deg,r] = frame_resize[y,x] else : img_pixels[deg,r] = [0,0,0] #画像の変換 img_dataRGB = np.zeros((height,width,3),dtype=np.uint8) for y2 in range(height): for x2 in range(width): R = img_pixels[y2,x2,2] G = img_pixels[y2,x2,1] B = img_pixels[y2,x2,0] #ガンマ補正 R = ((R/255.0)**gammaR)*255.0 G = ((G/255.0)**gammaG)*255.0 B = ((B/255.0)**gammaB)*255.0 #円中心と外側の明るさ補正 R = R*(x2+1)/(width) G = G*(x2+1)/(width) B = B*(x2+1)/(width) #整数値丸め R = round(R) G = round(G) B = round(B) img_dataRGB[y2,width-x2-1] = [R,G,B] fileName2 = str(fc).zfill(6)+'.bmp' print("\r{} {}/{} {}%".format(fileName2,fc+1,int(frameCount),round(fc/frameCount*100.0,2)), flush=True, end="") path_w = dirName+'\\'+fileName+'\\'+fileName2 img = Image.fromarray(img_dataRGB) img.save(path_w) cap.release() cv2.destroyAllWindows() print('Finish') |
今回お借りした動画
Arduinoプログラム
動画ファイル(連番BMP)が入っているフォルダ名は「BadApple_Color」、フレーム数は6578、FPSは30と元動画の情報を入力しています。6578/30 = 219秒 = 3分39秒の動画となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
#define GPIO_0to31_REG *((volatile unsigned long *)GPIO_OUT_REG) #define GPIO_0to31SET_REG *((volatile unsigned long *)GPIO_OUT_W1TS_REG) #define GPIO_0to31CLR_REG *((volatile unsigned long *)GPIO_OUT_W1TC_REG) #include <SPI.h> #include "SdFat.h" //SDカードの設定 const uint8_t SD_CS_PIN = SS; #define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(24))//24MHz SdFs sd; FsFile dir; FsFile file; uint32_t SDreadTime; //マルチスレッドのタスクハンドル格納用 TaskHandle_t thp; static uint8_t buf[512]; bool DataAvailable; bool DataReadable; bool DataSelect; uint32_t flameNum; // Define which pins to use. //SPI:18SCLK,23MOSI,19MISO,5CS //HSPI:14SCLK,13MOSI,12MISO,15CS #define sensorPin 27 //回転検出センサ入力 #define dataPin 13 //LEDクロック(HSPI使用時は不要) #define clockPin 14 //LEDデータ(HSPI使用時は不要) SPIClass hspi(HSPI); const bool SPIEnable = true; //SPIをLEDの通信に使うかどうか(false:GPIOをLEDの通信に使う) // Set the number of LEDs to control. const uint16_t LEDNum = 30; //LEDの数 const uint16_t circleNum = 283; //一周を何分解するか const uint16_t startDeg = 120; //回転検出センサの位置(0~359度) const bool CW = true; //回転の向き(false:CCW) // Set the brightness to use (the maximum is 31). uint8_t brightness = 8; //Timing to drawing unsigned long drawTime; unsigned long preTime; unsigned long nowTime; unsigned long cycleTime; unsigned long rawCycleTime; unsigned long cycleTimeTh = 20000; //誤検知防止用のしきい値[us] bool drawFlag; byte sendData[LEDNum*4+9]; byte imgByteA[circleNum][4*(uint16_t)ceil((float)LEDNum*3/4)];//Bitmapは列数を4の倍数にする必要あり byte imgByteB[circleNum][4*(uint16_t)ceil((float)LEDNum*3/4)]; //回転検出センサの割り込み void Interrupt(){ nowTime = micros(); rawCycleTime = nowTime-preTime; //回転検出センサの誤入力を除外 if(rawCycleTime>cycleTimeTh){ cycleTime = rawCycleTime; drawTime = cycleTime/circleNum; preTime = nowTime; drawFlag = true; } } void setup() { pinMode(sensorPin,INPUT); Serial.begin(115200); while (!Serial) { delay(1); // wait for serial port to connect. } //SDカードの認識 Serial.print("Initializing SD card..."); while(!sd.begin(SD_CONFIG)){ Serial.println("Card Mount Failed"); delay(1000); } Serial.println("Card Mount Succeeded"); //SPI設定、もしくはGPIOの出力設定 if(SPIEnable){ hspi.begin(14, 12, 13, 15); hspi.setFrequency(40000000); hspi.setDataMode(SPI_MODE3); hspi.setHwCs(true); } else{ pinMode(dataPin,OUTPUT); pinMode(clockPin,OUTPUT); } //割り込み設定 cycleTime = 500000; preTime = 0; //attachInterrupt(sensorPin, Interrupt, FALLING);//こっちだと回転数がブレる attachInterrupt(sensorPin, Interrupt, RISING); drawFlag = false; //LEDに送信するデータ配列の準備(開始Byte/終了Byte/明るさ) for(int i=0;i<sizeof(sendData);i++){ if(i<4){ sendData[i] = 0x00; } else if(i>=sizeof(sendData)-5){ sendData[i] = 0xFF; } else if(i%4 == 0){ sendData[i] = 224+brightness; } } //マルチコア設定 DataAvailable = false; DataReadable = false; DataSelect = true; xTaskCreatePinnedToCore(Core0a, "Core0a", 4096, NULL, 5, &thp, 0); // ウォッチドッグ停止 //disableCore0WDT(); delay(10); Serial.println("cycleTime,initialTime,memcpyTime,sendDataTime,SDreadTime"); } void loop() { drawSDMovie("BadAppleColor",6578,30); } //SDカード読み込み void Core0a(void *args) { byte* imgByte; uint32_t testTime; while(!drawFlag){ delay(1); } while(1) { if(DataReadable){ testTime = micros(); //Serial.println("DataRead"); DataAvailable = false; //flameNum分ファイルを飛ばす while(flameNum>0){ file.openNext(&dir, O_RDONLY); flameNum--; } if(!file){ Serial.println("Failed to open file"); } else{ //file.printName(&Serial); file.seek(54);//BMPの画素データ位置まで移動 if(DataSelect) file.read(imgByteB,sizeof(imgByteB)); else file.read(imgByteA,sizeof(imgByteA)); file.close(); DataAvailable = true; } DataReadable = false; DataSelect = !DataSelect; SDreadTime = micros()-testTime; } delay(1);//Watch Dog Timerエラー防止のため(Core0のWDT停止時には無効にできる) //yield();//不明なエラー防止のため(Core0のWDT停止時に有効にする) } } //SDカードからbmp動画を連続再生 void drawSDMovie(char* dirName,uint32_t fileNum,uint8_t fps){ uint32_t flame = 0; uint32_t preFlame = 0; uint32_t drawStartTime = millis(); while (!dir.open(dirName)){ Serial.println("Failed to open Dir"); delay(1000); } Serial.println("Sucess to open Dir"); dir.printName(&Serial); flameNum = 1; DataAvailable = false; DataReadable = true; while(1){ //drawFlag = true;//回転しないで表示する試験用 if(drawFlag){ drawFlag=false; //SDデータの読み込みが完了している場合 if(DataAvailable){ preFlame = flame; //現在のフレーム数を計算 flame = round((double)(millis()-drawStartTime)*(double)fps/1000.0); if(flame>=fileNum)break;//flameが最後まで行けば描画終了 flameNum = flame-preFlame; if(flameNum>0)DataReadable = true;//flameが進んでいない場合データは読み込まない } unsigned long nextDrawTime = micros(); //描画開始位置の設定 uint16_t startPos = round((float)circleNum * (float)startDeg/360.0); if(CW){startPos = circleNum-startPos;} uint16_t h = startPos; uint32_t testTime; uint32_t initialTime = 0; uint32_t memcpyTime = 0; uint32_t sendDataTime = 0; while(1){ if(nextDrawTime<=micros()){ testTime = micros(); nextDrawTime += drawTime; initialTime += micros()-testTime; testTime = micros(); for(int i=0;i<LEDNum;i++){ if(DataSelect){//現在がDataAの場合 sendData[4*i+5] = imgByteA[h][3*i+0]; sendData[4*i+6] = imgByteA[h][3*i+1]; sendData[4*i+7] = imgByteA[h][3*i+2]; } else{//現在がDataBの場合 sendData[4*i+5] = imgByteB[h][3*i+0]; sendData[4*i+6] = imgByteB[h][3*i+1]; sendData[4*i+7] = imgByteB[h][3*i+2]; } } memcpyTime += micros()-testTime; //LEDへデータを送信 testTime = micros(); if(SPIEnable){ hspi.writeBytes(sendData,sizeof(sendData)); } else{ writeBytes(sendData,sizeof(sendData)); } sendDataTime += micros()-testTime; if(CW){//時計回りの時 h--; if(h >= circleNum) h=circleNum-1; } else{//反時計回りの時 h++; if(h == circleNum) h=0; } if(h == startPos)break;//hが一周したら終了 } } Serial.printf("%ld,%ld,%ld,%ld,%ld\n",cycleTime,initialTime,memcpyTime,sendDataTime,SDreadTime); } yield();//不明なエラー防止のため } } void writeBytes(uint8_t *dat, uint16_t datSize){ for(uint16_t i=0;i<datSize;i++){ GPIO_0to31_REG = (dat[i]>>7&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>6&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>5&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>4&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>3&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>2&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>1&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH GPIO_0to31_REG = (dat[i]>>0&1)<<dataPin; //Clock:LOW GPIO_0to31SET_REG = 1<<clockPin; //Clock:HIGH } } |
Arduinoプログラムの補足
基本的な流れは第3回の方を参照してください。ここでは変更した所を中心に補足の説明をします。
動画の読み込み
191-196行:回転の始点でSDカードの連番BMPを読み込みを開始することになるのですが、動画の再生を開始してから時間から現在のフレーム数を計算して次に読む画像の番号を算出しています(正確には次に読む画像の番号と前に読んだ画像の番号の差)。
145-149行:計算されたフレーム数から次のファイルを読みます。連番BMPなので、ファイル名を直接指定して開くこともできるのですがこれをするとフォルダ内のファイル名の検索の時間が発生し、後半の画像になるほど読み込みまでに時間がかかります。そのため、file.openNext()で次の番号のファイルを開くということを(次のフレームになるまで)連続して行うことでこの問題を解決しています。
その他主な変更点
37,242-249行:前回、表示した画像が鏡写しになったので、その修正と回転方向(時計回り・反時計回り)の設定を追加しました。
165行:ウォッチドックタイマー(WDT)というものがあり、定期的にプログラムが動いているか監視しています。Core0ではこのWDTが働いており、While(1)で永久ループしている場合は定期的にdelay(1以上)を入れてその間にWDTに通信するということをしないとWDTエラーが発生してしまいます。Core1で動いているLOOPではWDTは働いていないようです。
256行:このyield()を入れないとCore1が動作しないため入れていますが、なぜうまく動かないのかは不明です。おそらく上記のWDTに絡んでいるように思われるのですが、WDTの関係のないCore1で発生し、しかもWDTへ影響しないと言われているyield()で解決するのかナゾです。分かる方いましたらコメントで教えてください。
動画の再生
回転が上がり切るまでの18秒付近までは(前回の回転数と次回の回転数が異なるため)画像が回転してしまいます。回転数がある程度安定してくると表示位置は固定されます。
POV(バーサライタ)で動画再生 pic.twitter.com/g1xyYdOqlp
— やまたい@ものつくり (@yamatai_mk) November 3, 2022
時間計測
最後に描画時間についての結果です。「LED一個あたりの時間」は「一周あたりの平均時間」をLED数と分解能で割った値です。
LEDへデータを送るSendDataの時間とSDカードのデータを読み込むSDReadの時間が大きめになっているため、POVが高解像度化するとここら辺の時間がネックになります。SDReadについては画像の圧縮で解決できる上、読み込みが間に合わなくともフレームレートが落ちるだけなのですが、SendDataに関してはLEDのクロック性能で頭打ちになるので、より速いLEDを探すかLEDアレイを分割して信号をパラレルに送信する必要があります。ただパラレルで送信する場合はSPIが1つしか使えないので、SPIでなくダイレクトGPIO(10MHz)で接続する必要があり、5列以上繋げないとパラレルで高速化することはできません。
LED数:30 分解能:283 SPI:40MHz SD:24MHz | Initial Core1 | MemCopy Core1 | SendData Core1 | SDRead Core0 | Core1合計 |
一周あたりの平均時間[us] | 238 | 768 | 10,124 | 13,680 | 11,130 |
LED一個あたりの時間[ns] | 28 | 90 | 1,193 | 1,611 | 1,311 |
コメント