NV21とは?YUV420 Semi-Planarの仕組みとAndroidカメラでの実装・変換・最適化完全ガイド

NV21 とは — 概要

NV21(エヌブイニジュウイチ)は、YUV 4:2:0(YUV420)のピクセルフォーマットの一種で、特にAndroidプラットフォームのカメラプレビューや画像処理の分野で広く使われているバイト配列フォーマットです。別名で「YUV420 Semi-Planar(YUV420sp)」や「YCbCr_420_SP」と表記されることもあります。Y(輝度)とUV(色差)成分を効率よく格納することで、メモリ使用量と帯域を抑えつつ視覚的に十分な色情報を提供します。

YUV420 と 4:2:0 の意味

まず基本となる「YUV420(4:2:0)」について整理します。YUVは輝度(Y)と色差(U=Cb、V=Cr)に分けた色空間です。4:2:0 とは、横方向・縦方向に対するサブサンプリング比を示し、輝度は画素ごとに保持される一方、色差は水平方向・垂直方向ともに2倍分解能を落としてサンプリングされます。結果として、1ピクセルあたりの平均ビット深度は12ビット(=1.5バイト/ピクセル)になります。これにより、フルカラー(RGB)よりもデータ量を削減できます。

NV21 のメモリレイアウト(バイト配列構造)

NV21 は「準プラナー(Semi-Planar)」フォーマットです。具体的には、配列は次の順で並びます。

  • 最初に幅×高さ分のY(輝度)成分(1バイト/画素)
  • 続いて幅×高さ/2 分の交互に並んだUV(色差)成分(2バイトで1組の色差)

重要なのは、クロマ成分の順序が「VU(V then U)」である点です。これがNV12(UVの順)との主要な相違点です。

例えば幅4、高さ2の小さな画像(計8ピクセル)の場合の配列イメージは次のようになります(Yが8バイト、VUは4バイト):

Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7 | V0 U0 V1 U1

上記でV0/U0は1つの2×2画素ブロックに対応する色差ペアを表します。

NV21 と他フォーマットとの対比

  • NV21 vs NV12:両者ともYUV420のSemi-Planarだが、NV21はVU順、NV12はUV順。データ順序が違うためそのままでは相互変換できない(バイトスワップや再配置が必要)。
  • NV21 vs I420(YUV420P/YV12):I420は「プラナー」で、Y、U、Vの3つの連続した平面が存在する(順序はY→U→V)。NV21はUとVがインターリーブ(交互)しているため、アクセス方法や性能面で差が出る。
  • NV21 vs YUV_420_888(Android):YUV_420_888 は抽象的で汎用的な3平面表現を提供するAPI側のフォーマットで、実際にはNV21やI420のような実装にマップされることが多い。Camera2 APIではYUV_420_888でImageを受け取り、必要に応じてNV21相当に変換して処理する。

Android における利用例・注意点

  • 従来の Camera API(古いAPI)では、onPreviewFrame のコールバックで NV21 形式の byte[] が渡されることが多く、YuvImage などでJPEGへ変換したり自前のY→RGB変換処理を行ったりするのが一般的でした。
  • Camera2 API(新API)では ImageReader を使い YUV_420_888 を受け取ることが推奨されますが、内部的に NV21 相当のバッファが得られるケースもあります。Image.Plane の rowStride / pixelStride の取り扱いに注意が必要です(幅と stride が異なる、行ごとのパディングが入ることがある)。
  • YuvImage のコンストラクタは NV21 をサポートしているため、カメラプレビューの byte[] を直接 JPEG に変換できるが、stride(行ごとのバイト数)が幅と異なる場合は適切に扱う必要があります。

ピクセル配置・stride(行幅)と crop の問題

実際のデバイスでは、各行のバイト数(row stride)が画面幅と一致しないことがあります。特にカメラAPIやハードウェアが内部アライメントのためにパディングを付ける場合があります。NV21配列をそのまま扱う際は次を確認してください。

  • Y 部分の rowStride が幅と一致しているか。
  • クロマ部(VU)の pixelStride が 2(隣り合うバイトが V/U交互)で、rowStride が幅(または幅と同じ偶数)でない可能性。
  • Image クラスで cropRect が設定されている場合、単純に幅×高さで切り出すだけでは正しくないことがある。

RGB への変換(色変換行列と式)

NV21 は Y, V, U の生データを与えるので、表示や解析のためには RGB に変換します。変換には色空間(BT.601, BT.709 など)と対応する係数が必要です。一般的な BT.601(標準ダイナミックレンジのSD向け)での式(浮動小数点)は次の通りです。

C = Y - 16
D = U - 128
E = V - 128

R = clip((298 * C + 409 * E + 128) >> 8)
G = clip((298 * C - 100 * D - 208 * E + 128) >> 8)
B = clip((298 * C + 516 * D + 128) >> 8)

上の式は整数演算に最適化された近似式(Yが16..235の限定レンジを仮定)。実装によっては Y を 0..255 として別の係数を使うこともあるため、カメラがどのレンジ/色空間を出力しているか確認するのが重要です。

変換の実装上のコツとパフォーマンス

  • CPUでバイト単位にループしてRGBを生成するとコストが高い(特に高解像度)。ネイティブ(NDK)でSIMDを使う、またはGPUシェーダでYUV→RGBを行うと高速化できる。
  • OpenGLやVulkanを使う場合、クロマを2チャンネル(RGテクスチャ)に格納してシェーダ内でサンプリングするテクニックが一般的。NV21のVUインターリーブはシェーダで扱いやすい。
  • Androidでは RenderScript(非推奨化の動きあり)やライブラリ libyuv、または GPU を利用するサードパーティ製の変換ライブラリが利用されることが多い。

典型的な用途

  • カメラのプレビューバッファ(リアルタイム処理、コンピュータビジョン、顔認識、バーコード読み取りなど)
  • ハードウェアエンコーダ(多くのエンコーダがYUV420を期待する)への入力
  • JPEG 生成(YuvImage を利用した変換)
  • ネットワーク転送時の帯域削減(RAW RGB よりデータ量が少ない)

注意すべき落とし穴・よくあるトラブル

  • VU と UV の混同:NV12 と NV21 の違いで色が反転(赤青が入れ替わる)などが起きやすい。
  • stride(行ごとのバイト数)や pixelFormat の違い:幅だけで配列を切り出すと上下・左右がずれることがある。
  • 色空間・レンジの違い:BT.601 と BT.709 を取り違えると色が不自然になる。特にHD映像やカメラの設定で変わる。
  • 並列変換時のメモリコピーコスト:リアルタイム処理では余分なコピーを避けるか、リングバッファで管理する。

実務上のアドバイス

  • 可能なら YUV_420_888 を使って各 Plane を明示的に扱い、stride と pixelStride を正しく取り扱う。これにより異なるデバイス間の互換性が高まる。
  • 高速化したい場合は、libyuv のような最適化済みライブラリや GPU シェーダを検討する。
  • 単純な JPEG 生成や簡便処理であれば、NV21 をそのまま YuvImage に渡すのが簡単で確実。ただし stride や crop を考慮する。
  • 色の正確さが重要なら、カメラが出力する色空間(Exif や CameraCharacteristics で確認可能)に合わせた変換行列を使用する。

まとめ

NV21 は Android を中心に広く使われる YUV420 の Semi-Planar フォーマットで、Y(輝度)と交互に格納された VU(色差)を持ちます。データ量が少なく、カメラプレビューやエンコーダ入力に適していますが、VU の順序、stride、色空間といった点に注意しないと色ずれやアライメントの問題に悩まされます。効率的な処理には、既成の最適化ライブラリや GPU を活用することを推奨します。

参考文献