2回に渡り、「#14-1 はい、チーズ(OV7670)」、「#14-2 いちたすいちは?に〜(OV7670)」と、写真を撮る時の掛け声をタイトルにしてきました。今回はOV7670の活用をテーマとした3回目、最終回です。
前回、OV7670から取得した画像の表示まで到達できて映像を確認することが出来たのですが、色合いがおかしな(正しくない)画像となっていました。今回は色合いのデバッグからスタートです。
前回の初期実験では、カラーバーが正常に表示されたことで、OV7670とTFTシールド間のピクセルフォーマット(RGB565)は合致していることが確認できました。他、OV7670レジスタの中で色調整に関わるものをデータシートから探してみたのですが、どうも、色変換行列(MTX1〜MTX6、MTXSレジスタ)の設定くらいしか見当たりません。
まず、一般的な色変換は以下式の様に行われます。
右から入って来たR,G,Bの成分値と3×3行列との掛け算をした結果、左辺のY,U,Vという色座標系に変換される仕組みです。OV7670では、以下の様な式でRGB → YUV変換を実施しています。
※Y/C分離信号にはYUVとかYCbCrとか言ったバリエーションが存在しますが、その変換は変換行列が異なるだけなので、変換先のY/C分離信号が何になるかはMTX1〜MTX6の設定値次第で変わります。
ここで、MTX1〜MTX6のパラメータは、同名のレジスタを使ってユーザが設定できるようになっています。リセット解除後のデフォルト値は以下の通りです。
レジスタ | リセット値 | 数値 |
MTX1 | 0x040 | 0.500 |
MTX2 | 0x134 | −1.594 |
MTX3 | 0x10c | −1.906 |
MTX4 | 0x117 | −1.820 |
MTX5 | 0x129 | −1.680 |
MTX6 | 0x040 | 0.500 |
MTXnレジスタの値は、9ビット符号付固定小数(小数点以下7ビット、×128倍)です。実際には、MTXnレジスタは8ビットレジスタなので、最上位の符号ビットだけを集めて、MTXSレジスタに格納しています。上表では便宜上、9ビットの16進数として表記しています。
MTXSレジスタには6ビットの設定ビットが存在し、第0ビット〜第5ビットがMTX1〜MTX6の符号ビットに割り当てられています。つまり、MTXnレジスタ毎に個別の符号ビットを格納できるようになっています。
改めて、一般の変換行列(式1)と、OV7670の変換行列(リセット値)を見比べてみると、だいぶ差がある様です。しかし、OV7670の撮像素子の特性を使って補正した値かも知れませんし、真偽のほどはわかりません。ただ、もしこの変換行列のせいで色合いがおかしいのだとすると、デフォルト(リセット値)の変換行列は信じるに値しません。
※Y/C分離信号にはYUVの他にもYCbCr、YIQ等のフォーマットが多数存在します。OV7670のデフォルト変換行列は、そのいずれとも一致しませんでした(調査した範囲内では)。
そこで、一般的な変換行列(式1)から始めて、色差成分U,VをU-V平面内で回転させながら、正しい色合いに見える行列を探してみようと思います。
上図は、YUV色座標系をU-V平面で切ってみた断面図です。変換行列には色々な補正が入っているみたいですが、基本的な考え方は以下の様になっています。
Y=0.299×R+0.587×G+0.114×B
U=B−Y
V=R−Y
これを踏まえて上の図を眺めてみると、
今、OV7670から読み出した画像は、赤い被写体が緑、緑の被写体が紫に見えているので、上図でU軸→V軸方向に135°程回転していると思われます。これを正しく補正するためには、逆に−135°回転すれば元に戻るはずです。
回転変換行列は以下の様になりますので、matr[θ]を色変換行列の左側から掛け算してやると、U-V平面内で回転した行列が求められます。
いつもの様にPi ZeroW+Mathematicaを使って計算させながら実験してみました(下図)。
実は単純に−135°の回転だけでは正しい色合いにはならなかったのだけど、色々試行錯誤の結果、ようやく良さそうな画像になった行列を眺めていて気が付きました。どうやら、変換式のR,G,Bが、B,R,Gにずれて対応しているようです。
何だかな〜。デフォルト(リセット値)の変換行列値が間違っているって、どんな設計しているのだろう?345円のカメラモジュールにこれ以上期待してもダメか……。その他色々試していて気が付いたけど、OV7670にはバグ🐛が多すぎるので、「余計な処理はさせない」に限るというのが結論です。
そこで、R,G,B → B,R,Gにずれて対応している前提に立ち、シンプルな変換行列を使うことにしました(下の表)。撮影画像の色調整は、PCなり何なりに取り込んだ後で、画像レタッチソフトウェア(拙作Olive+とか!)で調整すれば良いことだと割り切ります。
レジスタ | 設定値 | 数値 |
MTX1 | 0x000 | 0.000 |
MTX2 | 0x080 | 1.000 |
MTX3 | 0x180 | −1.000 |
MTX4 | 0x080 | 1.000 |
MTX5 | 0x000 | 0.000 |
MTX6 | 0x180 | −1.000 |
これにてようやく、まともな色で画像が表示されました。
ただし、青がほとんど発色しませんでした。OV7670の特性なのか、手元の個体がたまたま不良品なのか、原因は不明です。緑と赤系の被写体はそれなりに写ります。白い部分が、ちゃんと白に見えるので青の撮像素子が完全に死んでいるわけではなさそうです。やっぱりそういう「特性」なのかな?
色調整の変換行列以外にも、いくつか懸案として残していたOV7670のレジスタ設定値について確認しておきたいと思います。
まず、PCLKの周波数を決めておきます。
前々回検討した通り、PCLKの周波数はOV7670のCLKRCレジスタの設定値で調整できます。PCLKの周波数から、最終的に画像撮影のフレームレートが決まるので、可能な限りPCLKの周波数を上げることが目標です。
前々回別な節で検討した様に、PCLKの周波数を上げる場合にポイントとなるのは以下の二点でした。
更に、目視を基本とした上の様なチェック項目に加えて、定量的に評価可能な様に、TFTシールドへ描画する処理の実行時間をmicros()関数で測定します。プログラム先頭付近の「#define DEBUG_CAM_LINE」を有効にしてからコンパイル&書き込んでおくと、【itvl】コマンドなど、1フレーム取り込む度に以下の様なデバッグ用表示がシリアルモニタに出力されます(↓)。
DBG: cam_line: 240 [line] 606 (ave) 608 (max) [us/line]
DBG: cam_line: 240 [line] 606 (ave) 609 (max) [us/line]
DBG: cam_line: 240 [line] 595 (ave) 606 (max) [us/line]
:
数値は、1フレーム取り込む間に描画したライン数、ライン当たりの平均描画時間、最大描画時間、です。
上の表示は、実際に測定した結果ですので、この値を使ってPCLKの上限周波数を求めてみましょう。
TFTシールドへの描画時間は、歯抜けになった1ライン分の時間で実行されなくてはなりません。前々回解説した通り、1ラインの転送時間tLINE=784×2×tPCLKです。
上の表示結果を見る限り、最大の描画時間は609[μs/line]なので、ここから逆算すると、PCLKの周波数は2.57MHz未満にする必要があります。この場合のCLKRC≧2です。
∵fPCLK=fXCLK/(2×(CLKRC+1))<2.57MHz, fXCLK=14MHz
より、CLKRCについて解いた。
これが、要因2)に起因するPCLKの限界周波数なのですが、感覚的に速すぎると感じました。DUE @84MHzの処理速度をもってしても、2.57MHzのPCLKをサンプリングし続けるのは不可能です。つまり、恐らく要因2)でPCLK周波数が律速することはありません。
※TFTシールドへのアクセスでは、インラインアセンブラを使ってゴリゴリと高速化しておいたので、その甲斐があったといったところです。
残るは要因1)です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【itvl,1】
OK: itvl 1
【wrc,11,5】 …… CLKRCレジスタアドレス=0x11
OK: camera write [0x11]=0x05
CLKRC=5を設定しました。この時のfPCLK=1.17MHzです。その設定で取り込んだ画像が以下図です。
要因1)に起因する現象が見られます。所々でデータを取りこぼし、ノイズの様な横線を生じています。この設定値では、まだfPCLKが速すぎます。もっと遅くしないと……。
【wrc,11,6】、【wrc,11,7】、【wrc,11,8】、……と、だんだんCLKRCレジスタの設定値を大きくしながら、正常に取り込むことのできる限界を探りました。
結果、CLKRC=8〜9で正常に取り込むことが出来た様です。長い時間使用していると、もっとワーストなケースにぶち当たって、まれに失敗するケースがあるかもしれないので、少しマージンを見てCLKRC=0x0a〜0x0b辺りが無難でしょう。
CLKRC=0x0aに設定した場合、fPCLK=14MHz/(2×(0x0a+1))≒636kHz、tPCLK=1/fPCLK≒1.57μs、フレームレートtFRAME≒1.26sとなります。1フレーム(1画面)のデータ取り込みに1.26s要するという事なので、そんなに速くは無いですね。でも、DUEのMCUパワーではこの辺りが限界だと判りました。
一番最初に気が付いたのですが、横方向の画素数が足りませんでした。データシートに記載の通りにQVGAの出力設定をしたつもりなので、横方向の画素数は320ドット分取り込めるはずです。従って、画素数が320より少なかった場合にはエラーを表示するようにしてありました。すると、毎回エラー表示……。
常に同じ画素数を取り込んでいる様なので、PCLKを取りこぼしているわけでは無く、何らかの原因で出力されてくる画素数が少ないのだと思いました。
横方向の出力画素範囲を指定するレジスタがHSTARTレジスタとHSTOPレジスタなのですが、見直してみても設定値に誤りはないみたいです(↓)。
HSTART(アドレス0x11)=17 …… 17×8=136 (×8するとドット)
HSTOP(アドレス0x61)=97 …… 97×8=776 (同)
画素数=HSTOP−HSTART=776−136=640
※VGA(640×480[ドット])ベースの数値で設定するので、これで正解。
試しに、HSTARTの値を小さくして、結果として画素数=HSTOP−HSTARTが多くなる様にして見たのですが、なにも変化が見られませんでした(↓)。
実験サンプル:【 】内はシリアルモニタからの送信データ
【itvl,1】
OK: itvl 1
【wrc,17,10】 …… HSTARTレジスタのアドレス=0x17
OK: camera write [0x17]=0x10
思い切って、HSTARTレジスタに0x20とか、0x30とか書き込んでみましたが、全く変化が見られませんでした。このレジスタ、機能しているのかな??
次にHSTOPの値を大きくして、同じく画素数を増やしてみたところ、今度はちゃんと320ドット分取り込むことが出来ました(↓)。
【wrc,18,62】 …… HSTOPレジスタのアドレス=0x18
OK: camera write [0x18]=0x62
0x62を×8倍した値がVGAに対するドット数なので、HSTOP−HSTART=648ドットと、8ドット分多めに出力させる設定です。
ただし、以下図の様に、各フレームの先頭にHREFストローブのグリッチが出力されるようになっていて、別なエラーが発生してしまいました。
元に戻す【wrc,18,61】と、グリッチは消えます。……バグでしょ?これ。何だかな〜。
仕方ないので、上図の様なグリッチをロバストに無視できるようなソフトウェアにしたところ、エラーの発生を抑止して320ドットフルに取り込むことが出来るようになりました。
はあ〜、疲れた……。ユーザにデバッグさせなさんなって!
AGC、AECとは:
AGC(Automatic Gain Control、自動ゲイン制御)
AEC(Automatic Exposure Control、自動露出制御)
自動(Automatic)なので、基本的には、何もする必要がありません。ただし、各々、安定するまで数フレームを要すること、相互に影響し合ってしまうパラメータであること、等から、手動で調整しないと綺麗な画像にならない場合があります。
プログラム(qvga_cam.ino)のデフォルト設定では、AGC、AEC共有効になっています。【mec】コマンドや【mgc】コマンドを実行すると、AGC、AEC共無効になって、手動(Manual)制御となります。再びAECとAGCを有効化するコマンドは【auto,1】です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【itvl,1】
OK: itvl 1
【mec】
OK: mec 20
【mec,30】
OK+ AEC and AGC disabled.
OK: mec 30
【mec】コマンドをパラメータ無しで実行すると現在の設定値が表示されます。この項目は、カメラの露出(Exposure)を制御するので、画面が暗すぎる場合は設定値を大きく、画面が明るすぎる場合は設定値を小さくします。
実験サンプル:【 】内はシリアルモニタからの送信データ
【mgc】
OK: mgc 5
【mgc,10】
OK+ AEC and AGC disabled.
OK: mgc 10
【mgc】コマンドをパラメータ無しで実行すると現在の設定値が表示されます。この項目は、映像信号のゲイン(Gain)を制御するので、画面が暗すぎる場合は設定値を大きく、画面が明るすぎる場合は設定値を小さくします。
……なんだか、同じ説明になってしまいましたね……(1^^;)。うまく説明できませんが、「相互に影響する」ってことは理解していただけたでしょうか?初めは一方を固定して、他方がちょうど良い画質になる様に値を調整、そこから固定していた方のパラメータを振って最適値を探す、と言う感じでしょうか?
筆者も試してみたところ、晴れた日の明るい部屋の中で自動調整のままだと、画面が真っ白になって何も映りませんでした。撮影条件によって、自動調整が利く時とそうでない時がある様です。プログラム起動時のデフォルトは「自動調整が有効」なので、真っ白/真っ黒になってしまった場合は、【mec】、【mgc】コマンドを使い、マニュアルで調整して見てください。
そんなこんなで、OV7670の設計は全く信用できない(バグだらけ)ので、ガンマ補正についても見直すことにしました。
以下図に、OV7670のデフォルト(リセット値)設定のガンマ補正カーブと、一般的なγ=2.2の場合の補正カーブを示します。
横軸に入力信号、縦軸が補正後の出力信号です。
青プロットがOV7670のデフォルト設定値となっています。普通(γ=2.2)の曲線とはかなり異なっています。通常であれば、「ああ、撮像素子の特性に応じて適切な曲線をデフォルトに設定してあるのだな」と、肯定的に受け取ることが出来るのですが、OV7670に関しては、ダメです。
全く信用できないので、デフォルト値は無視して、γ=2.2の曲線を使います(以下コード)。
// gamma correction (gamma=2.2)
cam_write (0x7a,0x1e); // SLOP
cam_write (0x7b,0x26); // GAM1 ~ GAM15
cam_write (0x7c,0x34);
cam_write (0x7d,0x48);
cam_write (0x7e,0x63);
cam_write (0x7f,0x6e);
cam_write (0x80,0x77);
cam_write (0x81,0x80);
cam_write (0x82,0x88);
cam_write (0x83,0x8f);
cam_write (0x84,0x96);
cam_write (0x85,0xa3);
cam_write (0x86,0xaf);
cam_write (0x87,0xc5);
cam_write (0x88,0xd7);
cam_write (0x89,0xe8);
「筆者の個人的感想ですm(1__)m」。デフォルトで使いたい方は、ご自由にどうぞ。
以上の検討を経て完成したプログラムをリンクしておきます。
完成版プログラム:qvga_cam.ino
結構な大作になってしまいました。OV7670制御以外の部分は、過去に作ったプログラムからの流用なので、作っている本人は、そんなに大変ではなかったのですが、いきなりこのプログラムを解析しようと臨む方にとっては骨が折れるかもしれません(1^^;)。
基本的には、過去に作ったTFTシールド等のプログラムがベースなので、逆引き索引などを活用して、過去記事を当たってから戻って来てみてください。
こちらの回路を構築して最終実験に挑みます。以下、実験中の様子です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【itvl,1】
OK: itvl 1
ようやっと、ちゃんとした色で、OV7670から取り込んだ画像がTFTシールドに表示されました(下図)。
※モデルさんは筆者宅にお越しいただいたドロッセルお嬢様。
白いボディに赤いサングラスのモデルさんと、緑の万能基板が正しい色で撮影できました。
フレームレートは1.26[s/フレーム]程度なので、動きのある被写体だと、ブレてしまって写りません。ただ、画角やピントを調整する分には十分なフレームレートです。
※左端に黒い帯が出ていますが、HSTARTレジスタで調整しようとしても全く変化が見られなかったものです。これもOV7670のバグなんじゃ?
実際に動作させているところを動画にしてみました(↓)。フレームレート約1.26[s/フレーム]がどの程度のスピードか、ご確認いただけます。
何とか、バグだらけのじゃじゃ馬OV7670を乗りこなして写真撮影に成功しました焦電型赤外線(PIR)センサと組み合わせて防犯カメラにしたり、自動的にBMPファイル名を変えながら保存(撮影)してタイムラプス撮影にして見たり、タッチパネルのGUIを整備してデジカメにしてみたり、応用が広がると思います。