kCVPixelFormatTypeについての考察

iPhoneで動画像処理を行う場合kCVPixelBufferPixelFormatTypeKeyの値には
kCVPixelFormatType_32BGRA を用いるよりkCVPixelFormatType_420YpCbCr8BiPlanarVideoRangeが遥かに高速である。内部的には yuv->rgb 変換をやっているイメージ。一般的にピクセル単位の処理を行う場合、BGRチャンネルは扱いやすいが、YUV420で同様の処理を実装してみる。ARとかOpenCVとかやるときには結局輝度(Y)だけ使うことが多くてYUVのほうが都合が良いこともある。となるとこういう処理もたまには必要だろう。

YUV420のバイト配列は [YYYYYYYY・・・YYYYUVUVUV・・・UVUVUV]とのこと。つまり適当にピクセルのアドレスをセットした場合、
Y = *(src+y*width+x);
U = *(src+(y>>1)*width+(x>>1<<1)+width*height);
V = *(src+(y>>1)*width+(x>>1<<1)+width*height+1);
となるはず。

・・・ではないらしい。

この手法でピクセルデータを取得した場合、sessionPresetがAVCaptureSessionPreset640x480の場合は正常に動作しているのだが、AVCaptureSessionPresetHigh,AVCaptureSessionPresetMediumのとき、UVチャンネルがどうしても8px(uv値上は4px)ほどずれている。何らかのpadding処理かと思ってもルールが不明だった。ちなみにlowの場合、CVPixelBufferGetのサイズは480×360で返却されるが、上記の処理を行う場合は480×368にすると画像が正常にと表示される。ますます混乱する。

いろいろ調べたところ、CVPixelBufferGetBaseAddressOfPlaneの第2引数が使えるようだ。CVPixelBufferGetPlaneCountの値が2を返すことから2枚のPlaneとして構成されていると推測。

つまり
uint8_t *y = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
uint8_t *uv = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
である。うまくいった。

以下、座標x,yからrgb 取得するファンクション。
上で取得したYのアドレスとUVのアドレスを使えばOK

inline void getPixelColorAtPoint(int x,int y,color *out,uint8_t *src,uint8_t *srcUV=0 ){
 int t,Y,Cr,Cb;
 if(420YpCbCr8BiPlanarVideoRange){
 t = (y>>1) * mWidth + (x>>1<<1);
 Y = (-16 + *(src+ y * mWidth + x))*1.16438356;
 Cb = (-128 + *(srcUV+t))*1.1383928;
 Cr = (-128 + *(srcUV+t + 1))*1.1383928;
 out->r=clamp(Y+Cr+(Cr>>2)+(Cr>>3)+(Cr>>5));
 out->g=clamp(Y-((Cb>>2)+(Cb>>4)+(Cb>>5))-((Cr>>1)+(Cr>>3)+(Cr>>4)+(Cr>>5)));
 out->b=clamp(Y+Cb+(Cb>>1)+(Cb>>2)+(Cb>>6));
 }else{
 t=(y*mWidth+x)*mInDepth;
 out->b=*(src+t);
 out->g=*(src+t+1);
 out->r=*(src+t+2);
 }
}

CPPです。ちなみにレンジは Y’が 16–235。Cb/Crが 16–240。この辺は http://en.wikipedia.org/wiki/YUV を参考に書いてみたけど、ちょっと自信ない。clampしないとオーバーフローするし。なんとなくpreviewと色は合ってるのでまぁいいか。

ちなみに、このやり方だとUVチャンネルの読み取りに4倍コストかかるし、ポインタの連読性もないので、画像全体を描画するにはロスが大きい。スポイトのような用途で特定の1pxのBGR取得したい場合につかえます。そもそも画面全体を変換するならGPUに委譲すべきだね。