OV2640のイメージセンサの画素およびRAWデータ、RGBデータについて
まず、Omni Vision社のOV2640データシートによると、イメージセンサ自体は、1632×1232 pixel で、ベイヤー配列だそうです。
下図を見てください。
そのうち、有効な画素は、1632×1220 pixel で、垂直は12~1231列までが使用可能な画素です。
最初の12列分の画素は、使用不可なdummy画素と黒レベル補正に使われているそうです。
結局使えるのは、1632×1220 pixel で、その画素の中から自分の好きなようにフレームサイズに設定できます。
フレームサイズの最大は、1600×1200 pixel ( UXGA )です。
私は実はカメラやイメージセンサについてド素人なので、これを見て、「???」って思いました。
今まで私は液晶ディスプレイや有機EL(OLED)ディスプレイを扱ったことは何度もありますが、その場合、 Red, Green, Blue の全ての色が入って1pixelでした。
だから謎だらけなんです。
しかし、このベイヤー配列を見ると、Blueだけで1pixel、Greenだけで1pixelとなっています。
すると、最大解像度はその半分の800×600 pixel となってしまいます。
ということは、1600×1200 pixel という解像度はウソ? って思いますよね?
この訳は、実はこういうことです。
イメージセンサの場合、各受光素子には赤、青、緑のカラーフィルターが個別に貼ってあるそうです。
つまり、1つの受光素子には赤フィルターのみ。
もう一つの受光素子には青だけのフィルターのみ、という感じです。
その各素子の並び方が上図のような配置になっていて、その並びのことをベイヤー配列というそうです。
人間は緑色が特に敏感なので、緑の画素が赤や青よりも多くなっているとのこと。
その1画素は、その色の明るさの度合い(輝度)データしか受け取ることができないのです。
つまり、赤色の受光素子ならば赤色の明るさ(輝度)データのみ、緑色の受光素子は緑色輝度データのみ、青色素子ならば青色の輝度データのみしか受け取れません。
このそれぞれの色の輝度データのことを RAWデータというらしいです。
デジカメマニアでは良く知られている専門用語です。
(私はRAWという専門用語を今回初めて知りました。)
ただ、RAWデータだけでそのままディスプレイの画素に割り当てて表示させても、まともな画像は表示できません。
そこで、イメージセンサにはデジタルシグナルプロセッサ (DSP)が装備されていて、各画素の四方周囲の画素から予測変換計算して、その画素をフルカラーの画素 1pixel としてRGB出力してくれるという、便利な機能があるということです。
たとえば、Redだけの輝度データを持つ1画素から、周囲のGreen, Blue の輝度データから計算して、そのRed画素の位置のフルカラーデータをDSPで計算して、RGBフルカラーデータとして出力します。
結果的に、ベイヤー配列のRAWデータ画素数と同じになるので、イメージセンサのスペックとして、あくまでも疑似的に?最大解像度 1600×1200 フルカラーと表示しているようです。
画質はデジタルシグナルプロセッサ(DSP)の計算によって左右されてしまうため、RAWデータをパソコンの専用ソフトで現像した方が画質が良い場合があるようです。
確かに、RAWデータからRGBデータへ変換できるエンジン(RAW現像エンジン)を備えた画像編集ソフトであれば、イメージセンサの非力なDSPを使うよりも高画質が得られそうなことは容易に分かりますね。
とにかく、そんなに画質にこだわらなければ、OV2640のDSPだけで十分で、とっても便利です。
そんなこんなで、以上を理解しないでOV2640のデータシートをザッと読んでしまうと、とんでもない勘違いをしてしまうので要注意です。
データシートのPixel Output Patternという項目を読んでみると、以下のようなことが書いてあります。
表2は、OV2640からの出力データの順序を示しています。 最初のHREFに続くVSYNCの後のデータ出力シーケンスは、B、G、B、Gです。2番目のHREFの後、出力はG、R、G、Rなどになります。OV2640がSVGA解像度データを出力するようにプログラムされている場合 垂直方向のサブサンプリングが発生します。 1行目の出力のデフォルトの出力順序は、B、G、B、Gになります。2行目の出力は、G、R、G、Rになります。
ベイヤー配列およびRAWデータをある程度理解していないと、この文章を真に受けて、とんでもない間違いをしてしまいます。
私はデジカメの事はホントにド素人なので、この文章に何度もハマりました。
つまり、OV2640のDSP設定をRGBにした状態で出力される画素データは、1列目はB, G, B, G、2列目はG, R, G, Rという画素データが送られているはずだと思い込んでいたのです。
でも、それでプログラミングしてディスプレイに表示させてもまともな画像が表示されませんでした。
実際の所、1列目はRGB, RGB・・・というように各画素がフルカラーデータでした。
これが長い間疑問だったのですが、いろいろ調べていてつい最近RAWデータとRGBデータの違いが理解できて、ようやく納得するに至ったというわけです。
イメージセンサっていうのは、LCD等のディスプレイの画素とは全然違った仕組みで、私としては今更ながらとても勉強になりました。
OV2640の Sub-Sampling Mode ( SVGA, CIF )について
さて、今回は96×64 pixel のOLED SSD1331 に画像を収めねばなりませんが、UXGAサイズでは画像が大きすぎます。
そこで、OV2640には画質を下げる Sub-Sampling Mode というのがあります。
次に紹介する2種類、SVGAモードとCIFモードです。
SVGA Sub-Sampling Mode
前項の画像はUXGAモードでしたが、800×600 pixel のSVGAモードで画像データを出力する場合、下図の様に間引いてサンプリングされるそうです。
CIF Sub-Sampling Mode
OV2640の最小画角モードです。
今回は 96×64 pixel の OLED SSD1331 に収めるために、400×296 pixel のCIFというサブサンプリングモードを使います。
下図の様にサンプリングされます。
これはかなり間引いてサンプリングされるために、動画のようなフレームレートの速い、リアルタイム性を求めるものに向いるそうです。
OV2640 のフレームサイズ(画像サイズ)およびWindow設定
では、出力する画面サイズ(フレームサイズ)を決定していきます。
これは、OV2640 データシート独自和訳と英語版データシートを参照しながら読み進めていってください。
OV2640のSub-Sampling Modeで画角を最小のCIFモード 400×296 pixel まで落としたとして、OLEDディスプレイ SSD1331 の96×64 pixelサイズに収めるためにはもっと画角を小さくせねばなりません。
そのための、フレームサイズ(画像サイズ)を設定していきます。
実はこの設定が、とぉーってもややこしい!!!、そして難解、そして面倒なのです。
まず、OV2640データシートに書いてあるように、Windowingという設定をします。
先にも述べたように、OV2640のイメージセンサで有効な画素は1632×1220 pixelですが、その中から自分の好きな位置でフレームサイズを決められます。
OV2640データシートによれば、
OV2640では、アプリケーションの要求に応じて、ユーザーはウィンドウサイズまたは関心領域(ROI)を定義できます。
ウィンドウサイズの設定(ピクセル単位)は、2×4 pixel から1632×1220 pixel(UXGA)まで、又は、2×2 pixel から818×610(SVGA)まで、および408 x 304(CIF)の範囲で、1632 x 1220境界内のどこでも構いません。
ウィンドウサイズやウィンドウ位置を変更しても、フレームレートやピクセルレートは変わりません。
ウィンドウ制御は、プログラムされた水平および垂直ROIと一致するようにHREF信号のアサーションをただ単に変更するだけである。
デフォルトのウィンドウサイズは1600 x 1200です。
詳細については、図4およびレジスタHREFST、HREFEND、REG32、VSTRT、VEND、およびCOM1を参照してください。
そして、OV2640 Camera Module Software Application Notes の中の、RGB565 Reference Setting の設定例を参照して、フレームサイズ設定に関わる項目を抜粋して見てみます。
以下のところです。
これは、SVGA(800×600)サイズのフレーム画面の場合です。
write_SCCB(0x17, 0x11); //HREFST write_SCCB(0x18, 0x43); //HREFEND write_SCCB(0x19, 0x00); //VSTRT write_SCCB(0x1a, 0x4b); //VEND write_SCCB(0x32, 0x09); //REG32 /*.................. 途中省略 */ write_SCCB(0xc0, 0x64); //HSIZE8[7:0] write_SCCB(0xc1, 0x4B); //VSIZE8[7:0] write_SCCB(0x8c, 0x00); //SIZEL[5:0] write_SCCB(0x86, 0x3D); //CTRL2 write_SCCB(0x50, 0x00); //CTRLl ※CTRL1と混同し易いので注意 write_SCCB(0x51, 0xC8); //H_SIZE[7:0] write_SCCB(0x52, 0x96); //V_SIZE[7:0] write_SCCB(0x53, 0x00); //XOFFL[7:0] write_SCCB(0x54, 0x00); //YOFFL[7:0] write_SCCB(0x55, 0x00); //VHYX write_SCCB(0x5a, 0xC8); //ZMOW write_SCCB(0x5b, 0x96); //ZMOH write_SCCB(0x5c, 0x00); //ZMHH write_SCCB(0xd3, 0x82); //R_DVP_SP
これを解明するのは、かなり難しかったです。
私自身、未だに良く分かっていませんので、間違えていたらスイマセン。
この設定はデータシートを読み込んでも全然理解できなかったので、Arduino core for the ESP32 の esp32-cameraライブラリを解読した方が遙かに分かりやすいです。
set_framesize関数を解読していけば良いと思います。
それでも解明するのはとっても大変でした。
私が独自にset_framesize関数を改変して抜粋してみたコードは以下の感じです。
(単体では動作しませんのでご注意ください)
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
/* This is mgo-tec modified sccb.c of esp32-camera library. * sccb.c file is part of the OpenMV project. * Copyright (c) 2013/2014 Ibrahim Abdelkader. * This work is licensed under the MIT license. * URL: https://opensource.org/licenses/mit-license.php */ const uint16_t sensor_resolution_h = 400, sensor_resolution_v = 296; //CIF mode int setFramesize(uint16_t out_width, uint16_t out_height){ int ret = 0; ret = writeSCCB(0xFF, 0x00);//bank dsp if (!ret) { ret = writeSCCB(0x05, 0x00); //R_BYPASS:0x00 DSP use. if(ret) return ret; } delay(5); uint8_t pclk_div2 = 0x01; //PCLK clock divider 2 uint16_t window_h_start = 137; uint16_t window_h_end = window_h_start + sensor_resolution_h; uint16_t window_v_start = 2; uint16_t window_v_end = window_v_start + sensor_resolution_v; uint8_t win_h_st_bit10_3 = (uint8_t)((window_h_start >> 3) & 0x00ff); uint8_t win_h_end_bit10_3 = (uint8_t)((window_h_end >> 3) & 0x00ff); uint8_t win_v_st_bit9_2 = (uint8_t)((window_v_start >> 2) & 0x00ff); uint8_t win_v_end_bit9_2 = (uint8_t)((window_v_end >> 2) & 0x00ff); uint8_t win_h_st_bit2_0 = (uint8_t)(window_h_start & 0x0007); uint8_t win_h_end_bit2_0 = (uint8_t)(window_h_end & 0x0007); uint8_t win_v_st_bit1_0 = (uint8_t)(window_v_start & 0x0003); uint8_t win_v_end_bit1_0 = (uint8_t)(window_v_end & 0x0003); writeSCCB(0xFF, 0x01); //BANK:sensor delay(5); writeSCCB(0x12, 0b00100000); //COM7 [6:4]CIF mode writeSCCB(0x17, win_h_st_bit10_3); //HREFST(default:0x11) writeSCCB(0x18, win_h_end_bit10_3); //HREFEND(SVGA,CIF default:0x43) writeSCCB(0x19, win_v_st_bit9_2); //VSTRT writeSCCB(0x1A, win_v_end_bit9_2); //VEND writeSCCB(0x32, 0x00 | (pclk_div2 << 7) | (win_h_end_bit2_0 << 3) | win_h_st_bit2_0); //REG32,[7:6]10:PCLK frequency devide by 2, [5:0]0x09:CIF writeSCCB(0x03, 0x00 | (win_v_end_bit1_0 << 2) | win_v_st_bit1_0); //COM1 0x0A:CIF? writeSCCB(0x11, pclk_div2); //CLKRC 1/2 clock divider delay(5); uint16_t HSIZE = sensor_resolution_h; //Image Horizontal Size uint16_t VSIZE = sensor_resolution_v; //Image Vertical Size uint16_t H_SIZE = HSIZE / 4; uint16_t V_SIZE = VSIZE / 4; uint8_t H_SIZE_bit7_0 = (uint8_t)(H_SIZE & 0x00ff); uint8_t V_SIZE_bit7_0 = (uint8_t)(V_SIZE & 0x00ff); uint8_t H_SIZE_bit8 = (uint8_t)((H_SIZE >> 8) & 0x0001); uint8_t H_SIZE_bit9 = (uint8_t)((H_SIZE >> 9) & 0x0001); uint8_t V_SIZE_bit8 = (uint8_t)((V_SIZE >> 8) & 0x0001); uint8_t zoom_speed = 0; uint16_t OUTW = (uint16_t)floor((double)out_width / 4.0); uint16_t OUTH = (uint16_t)floor((double)out_height / 4.0); uint8_t OUTW_bit7_0 = (uint8_t)(OUTW & 0x00ff); uint8_t OUTH_bit7_0 = (uint8_t)(OUTH & 0x00ff); uint8_t OUTW_bit9_8 = (uint8_t)((OUTW >> 8) & 0x0003); uint8_t OUTH_bit8 = (uint8_t)((OUTH >> 8) & 0x0001); uint8_t HSIZE_bit11 = (uint8_t)((HSIZE >> 11) & 0x0001); uint8_t HSIZE_bit2_0 = (uint8_t)(HSIZE & 0x0003); uint8_t VSIZE_bit2_0 = (uint8_t)(VSIZE & 0x0003); uint8_t HSIZE_bit10_3 = (uint8_t)((HSIZE >> 3) & 0x00ff); uint8_t VSIZE_bit10_3 = (uint8_t)((VSIZE >> 3) & 0x00ff); Serial.printf("HSIZE=%d, VSIZE=%d, H_SIZE=%d, V_SIZE=%d, OUTW=%d, OUTH=%d\r\n", HSIZE, VSIZE, H_SIZE, V_SIZE, OUTW, OUTH); writeSCCB(0xFF, 0x00); //BANK:DSP delay(5); writeSCCB(0xE0, 0b00000100); //RESET bit[2]:DVP ※DVPはパラレルデジタルフォーマット delay(5); writeSCCB(0xC0, HSIZE_bit10_3); //HSIZE8は11bitのうちの上位8bit。これはh_pixel 400 writeSCCB(0xC1, VSIZE_bit10_3); //VSIZE8は11bitのうちの上位8bit。これはv_pixel 296 writeSCCB(0x8C, 0x00); //SIZEL 使い方は不明 writeSCCB(0x86, 0b00111101); //CTRL2 writeSCCB(0x50, 0b10000000); //CTRLl LP_DP EN (※CTRL1と混同し易いので注意) writeSCCB(0x51, H_SIZE_bit7_0); //H_SIZE writeSCCB(0x52, V_SIZE_bit7_0); //V_SIZE writeSCCB(0x53, 0x00); //XOFFL writeSCCB(0x54, 0x00); //YOFFL writeSCCB(0x55, 0x00 | (V_SIZE_bit8 << 7) | (H_SIZE_bit8 << 3)); //VHYX writeSCCB(0x57, (H_SIZE_bit9 << 7) | 0x00); //TEST writeSCCB(0x5A, OUTW_bit7_0); //ZMOW[7:0] (real/4) writeSCCB(0x5B, OUTH_bit7_0); //ZMOH[7:0] (real/4) writeSCCB(0x5C, 0x00 | (zoom_speed << 4) | (OUTH_bit8 << 2) | (OUTW_bit9_8)); //ZMHH writeSCCB(0xD3, 0b10000010); //R_DVP_SP writeSCCB(0xE0, 0x00); //RESET writeSCCB(0x05, 0x00); //R_BYPASS:0x00 DSP use. delay(10); //解像度を変更する場合、これは必要らしい return ret; }
まず、OV2640 のレジスタ 0xFF でBANKを0x01にして、sensor設定にします。
そして、今回はPCLKの周波数をClock Divider(クロック分割)でXVCLKの20MHzシステムクロックより1/2落として、10MHzとします。
そして、先ほど述べた Windowing 設定をします。
要するに、ベイヤー配列のセンサーから、自分の注目領域(ROI)に絞るわけです。
実はこれがとても難解です!!!。
まず、レジスタ 0x12 でCIFモードにします。
そして、水平の注目領域(ROI)を137~537と決めます。
垂直は 2~298 と決めます。
CIFモードの場合、なぜそのWindow領域にするのかはサッパリ分かりません。
例えば、137というスタート位置を100とすると、画面が空白になったりして、うまくいきません。
これがほんとに疑問なんです。
データシートのWindowingの項目のところに記載してあったように、1632 x 1220 pixel内であればどこでも良いはずです。
それに、データシートに書いてあるように、CIFモードならば水平2~408、垂直2~304 の範囲のはずでは???
水平137~537っておかしいじゃないないですかっっっ!!!
でも、なぜか、水平137~537、垂直2~298 でうまく行くんです。
もう謎だらけで分かりませーーーん!!!
ということでこんなところで停滞していると気が狂いそうなので、先へ進みます。
水平開始位置や水平終了位置は11bitで表現します。
でも、OV2640 のレジスタ設定では8bit (1byte)単位でしか動かせないので、他のレジスタ設定値と一部分と合成させて、11bitを表現します。
水平開始位置 137 を2進数に変換して、下図の様に上位8bit (MSB) と下位3bit (LSB)に分けて、レジスタ0x17 (レジスタ名HREFST) と、レジスタ0x32 (レジスタ名REG32) のbit[2]~bit[0]に設定します。
水平終了位置は CIFモードの場合、400×296 pixel なので、
137 + 400 = 537
となります。
上記と同じように、2進数に変換して11bit表記とし、上位8bitをレジスタ0x18(レジスタ名:HREFEND)、下位3bitをレジスタ0x32(レジスタ名:REG32)のbit[5]~bit[3]に設定します。
さて、ここで疑問が湧きませんか??
プログラミングの場合、400pixelとしたら、137~536なのでは???
まぁ、あまり考えてもこれでうまく行くので、ここは謎のままにしておきます。
次に、垂直位置は注意が必要で、10bit 表現です。
なぜ 10bit ???
だって、UXGAサイズならば垂直は1200pixel じゃないですか!!!
1200 という数値は11bitのはずでは???
大いに疑問ですが、今回はUXGAなんて大きいサイズは扱えないので、謎のまま放置しておいて、先へ進みます。
垂直開始位置の場合、下図の様に10bitのうち、上位8bitをレジスタ 0x19 (VSTRT) にし、下位2bitをレジスタ 0x03 (COM1)の bit[1]~bit[0]に設定します。
同じように、垂直終了位置は、上位8bitをレジスタ0x1A(VEND)にし、下位2bitをレジスタ0x03(COM1)のbit[3]~bit[2]に設定します。
以上でOV2640のCIFモードのWindowing設定ができました。
今度はレジスタ0xFFでBANKをDSP(0x00)設定にます。
そして、いよいよ出力するフレームサイズの設定ですが、先に紹介したように、レジスタをまたぐ小難しいビット設定ばかりです。
まず、レジスタ0xC0 とレジスタ0xC1 を設定を見てください。
ここからが更にややこしいです。
レジスタ0xC0 のレジスタ名はHSIZE8、レジスタ0xC1のレジスタ名はVSIZE8です。
OV2640データシートには、
Image Horizontal Size HSIZE[10:3]
Image Vertical Size VSIZE[10:3]
と書いてあります。
はて?
これはどのイメージサイズなんでしょうか?
CIFモードのサイズなんでしょうか?
更に惑わされるのが、レジスタ0x51のHSIZEとレジスタ0x52のVSIZEがあります。
その説明覧に、
H_SIZE[7:0] (real/4)
V_SIZE[7:0] (real/4)
と書いてあります。
それに更にさらに、レジスタ0x5AのZMOWとレジスタ0x5BのZMOHの説明覧には、
OUTW[7:0] (real/4)
OUTH[7:0] (real/4)
と書いてあります。
このrealというのはどのイメージサイズを示しているんでしょうか?
最初は何のことだかサッパリ分かりませんでした。
仕方ないので、OV2640 Camera Module Software Application Notes のUXGAサイズとSVGAサイズの設定例を見比べてみたり、設定を変えて何度もコンパイル書き込み実行を繰り返したりしました。
そしたら何となく分かってきました。
OV2640データシートのレジスタ説明覧だけに絞って見てみると、水平サイズには HSIZE とH_SIZE という2種類の表記がありました。
これは同じ値だとずっと思い込んでいたんですが、どうやら異なるものと思われます。
HSIZE はOV2640の3種類の解像度(UXGA, VXGA, CIF)のうちの何れかのサイズしか入力できなくて、H_SIZEはHSIZEの1/4の値ではないかと思われます。(間違えていたらスイマセン)
ですから、CIFモードの場合、
HSIZE = 400
VSIZE = 296
H_SIZE = HSIZE/4 = 100
V_SIZE = VSIZE/4 = 74
となるのではないでしょうか?
HSIZEやVSIZEは自分の好きな値に変えられないようで、UXGA, SVGA, CIF の何れかのサイズ限定のようです。たぶん・・・。
そして、OUTW という値は、実際に自分自身が欲しいフレームサイズ、つまりGPIOからパラレル通信データを送信するフレームサイズの1/4を指定するということのようです。
このOUTWとOUTHでOLED SSD1331ディスプレイの96×64 pixelに収める設定をするわけです。
ただ、OUTWやOUTHのサイズはCIFモードの場合、縦横アスペクト比を維持していないと出力してくれません。
そこで、今回使うOLED SSD1331ディスプレイは96×64 pixelですから、それに近い値となると、100×74 pixel となります。
よって、
OUTW = 100/4 = 25
OUTH = 74/4 = 18.5
となります。
18.5という小数点以下の数値は入力できないので、小数点以下を切り上げるか切り下げるかします。
どちらにした方が良いかというと、ディスプレイ SSD1331 は96×64 pixel なので、それに近い値にすると、切り下げる方が良いです。
すると、OUTHは、
OUTH = floor(74.0/4.0) = 18
とします。
この設定にすると、OV2640から 100×72 pixel の画像データを出力してくれます。
CIFモードの場合、本当は100×74 pixelで出力した方が良いのですが、74は4で割り切れないので、仕方なく100×72 pixelとします。
そして、OLED SSD1331 でディスプレイ表示する際には、余った画素を捨てて表示すれば良いわけです。
OUTWやOUTHの値は、レジスタ0x5A, 0x5B, 0x5C で設定して、先ほど説明したWindowingと同じようにレジスタを跨いでビットを設定していく形になります。
OUTWやOUTHは、レジスタ名ZMOWやZMOHで、アスペクト比を保ちながら数値を大きくしていくと、ズームして画像を拡大します。
CIFモードの場合の等倍率の場合は、先ほど説明したように、
OUTW = 100/4 = 25
OUTH = floor(74.0/4.0) = 18
となります。
2倍ズームにしたい場合、
OUTW = 200/4 = 50
OUTH = 148/4 = 37
4倍ズームは
OUTW = 400/4 = 100
OUTH = 296/4 = 74
となります。
それ以上の数値にすると、なぜか画像を出力してくれませんでした。
その理由は、OV2640 データシート独自和訳のWindowing項目の中の、Zooming and Panning Mode を見ると書いてあります。
ズーム率はCIFモードでは4倍までとなっています。それに、OUTW や OUTH は real/4 と書いてあるので、恐らく出力したい画像のサイズはCIFモードのサイズを超えられないのかも知れません。
因みに、このOUTW や OUTHのサイズを変えてもフレームレートには影響が無いようです。
これについては後で述べます。
その他、レジスタ0x8C のSIZEL の使い方はイマイチよくわかりません。
それと、レジスタ0x50はCTRLlというレジスタ名ですが、似たようなレジスタ名CTRL1というのがあり、混同しないように注意して下さい。
小文字アルファベットのエル’l’ と数値の’1’と、見た目とっても間違いやすいです。
気をつけたいですね。
コメント
Hi aMiGO,
Excellent job! I appreciate it very much.
Although you don’t know much about DMA, you have come a long way.
Congratulations
Thanks
Gustavo Murta (from Brazil)
amigo (portuguese) = friend
Thanks for watching this blog post!!!