NTSCビデオ信号をデコードしたい

## NTSCとは

特に説明の必要もないと思いますが、カラービデオの信号の規格の一つです。僕はよくNTSBとNTSCがごっちゃになりますが、映像信号の方です。(NTSBとNTSCの見分け方：NTSBのBはBroadcastじゃない)。

元々のテレビ放送は白黒の輝度信号だけを放送していました。その後でカラー放送を行いたい、となったときに、いくつか作られた規格の内の一つです。
日本ではアナログ放送は10年近く前にすでに放送が終了しているので今更感は凄まじいですが、車のバックモニタとか、最近ではドローンのFPV用にNTSC信号のカメラが供給されているようです。
特殊な用途だと、IKAROSやはやぶさ2のDCAMや、そのシステムを流用した小型衛星等がNTSC出力のカメラを使っているらしいです(たしかデコードはアナデバの民生デコードICを使っていたような気がします)。

## データソース

今回はFPV用のカメラから映像信号をオシロスコープでキャプチャしました。

こんな感じの信号です。

![2020-07-23_22-16-01 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/fe16af52-b216-7903-88ae-9c5ae76e85b8.png)

終端抵抗をつけずにプロービングしているので振幅が高く出ていますね。
Siglentのオシロはメモリが長くてEthernet経由でバイナリ波形を読み出せるので、アナログ波形をチャチャッと取り出したいときは便利です。

オシロの機能でビデオ信号にトリガできるので、1フィールド目の1ライン目の先頭から波形データが始まるようにサンプリングしています。そのため、今回は同期信号の認識を行う必要はなく、その周りの話題は扱いません。

## NTSCの定数

NTSCで一番重要なのが色信号の基準となるカラーサブキャリア(fsc)で、約3.58MHzです。実際は315M/88です。
また、水平同期(fH)がfsc/227.5=約15.7kHz、垂直同期(fV)がfH/262.5=約59.94Hz、インターレースなので実際のフレームレートは半分で約29.97Hz、となります。
現在でも映像信号は29.97fpsや59.94fpsが多いですが、この数値は白黒放送とカラー放送の互換性を維持する際に行われた工夫に由来しています。

蛇足ですが、ファミコンのクロックが1.79MHzなのは、NTSCの3.58MHz/2に由来しているようです。ファミコンで表示できる色の数も、NTSCで作りやすい色が選ばれています。ファミコンの頃は計算能力に余裕が無く、NTSCの影響がかなり大きく現れています。

## 実際にデコードする

今回はC#(.Net FW)で処理します。自分が使い慣れているというだけの理由です。

波形データは適当な中間形式を経て、`float[] IREs`に突っ込んであります。IREの名前の通り、NTSCのIREレベルに合わせてあります。

なお、説明を簡単にするために、かなり端折った説明をしています。
というか、書いている僕自身、仕事で映像信号を扱ったりといった経験は全く無く、基本的にググって出てきた話を鵜呑みにしているだけなので、間違っている部分も多分に含まれると思われます。ご了承ください。

### とりあえず白黒画像を取り出す

NTSCはカラー放送を白黒テレビで受信した際でも白黒映像として表示できるように互換性をもたせてあるので、白黒画像としてデコードするだけなら簡単に処理できます。
具体的には、適当な大きさのBitmapを用意して、逐次アナログデータを書き込んでいくだけです。

アナログ波形は50Mspsでサンプリングされているので、NTSCのfHから、横幅は50Msps/fH=3178pxになります。

```c#
int pointsPerLine = (int)Math.Round(sps / NTSC.fH);

Bitmap bmp = new Bitmap(pointsPerLine, IREs.Length / pointsPerLine);

for (int y = 0; y < bmp.Height; y++)
{
    int i = (int)Math.Round(sps / NTSC.fH * y);

    for (int x = 0; x < bmp.Width; x++)
    {
        float Y = IREs[i + x] / 100.0f;
        int W = (int)(Y * 256);
        W = W < 0 ? 0 : 255 < W ? 255 : W;

        bmp.SetPixel(x, y, Color.FromArgb(W, W, W));
    }
}

bmp.Save("log.png");
```

こんな感じの画像が出てきます。

![log.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/557374b3-4219-742b-bcbf-4b78a2eb17f4.jpeg)

(横3千px以上は流石にデカイのでリサイズしています)

縦横比が崩れているので何がなんだかわからないですね。

横分解能を1fscあたり4pxとすると、50M/(fsc*4)=3.5くらいなので、横幅を3.5分の1に縮小し、インターレース分で高さを2倍に拡大すると、おおよそいい感じの縦横比になります。

![log.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/9804a57b-e035-7637-6703-a02e986ccd9b.jpeg)

ちゃんと画像として見えるようになりましたね。
映像信号と言っても、アナログ映像であれば実態は単純な電圧データなので、いい感じに2次元に並べてあげれば簡単に画像として見えるようになります。映像信号恐るるに足らず!という感じですね。この調子で図に乗ったままチャチャッと進めちゃいましょう。

### もう少し真面目に白黒画像を取り出す

-40IREを黒、100IREを白、として画像化して一部を切り出すとこんな感じになります。

![log.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/3cc9dc00-9ca6-8509-701b-2aaad73bb1c3.jpeg)

映像の左端のバースト信号や本の表紙の部分に市松模様のようなザラザラが出ています。これが色信号の実態です。カラー映像を白黒映像として見ているためにこのようなノイジーな状態になってしまいます。

色信号は正弦波ですが、これは1行毎に位相が180度ずらされているため、1行上のラインとの平均値を取れば、正弦波成分を除去できます。

`DelayLine<T>`というクラスを作って、データを入れるとNポイント後にそのデータが出てくる、というフィルタを作っておきます(フィルタというか、純粋に遅延です)。

```c#
int pointsPerLine = (int)Math.Round(sps / NTSC.fH);
DelayLine<float> dl = new DelayLine<float>(pointsPerLine);

Bitmap bmp = new Bitmap(pointsPerLine, IREs.Length / pointsPerLine);

for (int y = 0; y < bmp.Height; y++)
{
    int i = (int)Math.Round(sps / NTSC.fH * y);

    for (int x = 0; x < bmp.Width; x++)
    {
        float ire = IREs[i + x];
        float a = dl.Process(ire);

        float Y = (ire + a) / 2.0f / 100.0f;
        Y = (Y * 100 + 40) / 140.0f; // -40IREを黒に、0IREをグレーに、100IREを白に、全体的に持ち上げる

        int W = (int)(Y * 256);
        W = W < 0 ? 0 : 255 < W ? 255 : W;

        bmp.SetPixel(x, y, Color.FromArgb(W, W, W));
    }
}

bmp.Save("log.png");
```

![log.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/81b903be-9ace-ac2c-03b8-75aa82006418.jpeg)

完全に色信号を除去できるわけではありませんが、ザラザラ感はかなり解消できました。

### いよいよ色情報を取り出す

さて、色情報を除去することができるようになりました。ということは、除去した分を持ってくれば色情報を取り出せる、というわけです。
色信号はQAMで変調されており、それが輝度信号に加算されているので、上の行との和で輝度が、差でクロマ(彩度+色相)が得られます。QAMの基準位相(バーストサイクル)は水平ブランキングエリアに含まれているので、これも取り出す必要があります。この基準位相は180度の位置にあるので、PIを足して0度に戻してから利用します。
バーストサイクルの位相がわかれば、クロマに掛け合わせることでU相/V相に分離できます。クロマから取り出したUVと輝度からRGBを復元できるので、それを画像として出力します。

```c#
int pointsPerLine = (int)Math.Round(sps / NTSC.fH);
DelayLine<float> dl = new DelayLine<float>(pointsPerLine);

int pointsPerColorCycle = (int)Math.Round(sps / NTSC.fsc);
FIR firU = new FIR(Enumerable.Range(0, pointsPerColorCycle).Select(_ => 0.436f / pointsPerColorCycle).ToArray());
FIR firV = new FIR(Enumerable.Range(0, pointsPerColorCycle).Select(_ => 0.615f / pointsPerColorCycle).ToArray());

Bitmap bmp = new Bitmap(pointsPerLine, IREs.Length / pointsPerLine);

int burstStart = (int)Math.Round(sps / NTSC.fsc * 19);
int burstEnd = (int)Math.Round(sps / NTSC.fsc * (19 + 9));
double colorSubcarrierAngleSpeed = NTSC.fsc / sps * Math.PI * 2;

for (int y = 0; y < bmp.Height; y++)
{
    int i = (int)Math.Round(sps / NTSC.fH * y);

    double Re = 0;
    double Im = 0;
    for (int x = burstStart; x < burstEnd; x++)
    {
        Re += IREs[i + x] * Math.Cos(x * colorSubcarrierAngleSpeed);
        Im += IREs[i + x] * Math.Sin(x * colorSubcarrierAngleSpeed);
    }

    double rad = Math.Atan2(Re, Im) + Math.PI;

    for (int x = 0; x < bmp.Width; x++)
    {
        float ire = IREs[i + x];
        float a = dl.Process(ire);

        float Y = (ire + a) / 2.0f / 100.0f;
        float C = (ire - a) / 2.0f / 40.0f;
        float U = firU.Process(C * (float)Math.Sin(rad + x * colorSubcarrierAngleSpeed));
        float V = firV.Process(C * (float)Math.Cos(rad + x * colorSubcarrierAngleSpeed));

        int R = (int)(255 * (Y + V * 1.13983));
        int G = (int)(255 * (Y + U * -0.39465 + V * -0.5806));
        int B = (int)(255 * (Y + U * 2.03211));

        bmp.SetPixel(x, y, Color.FromArgb(
            R < 0 ? 0 : 255 < R ? 255 : R,
            G < 0 ? 0 : 255 < G ? 255 : G,
            B < 0 ? 0 : 255 < B ? 255 : B));
    }
}

bmp.Save("log.png");
```

こうして得られた画像のアス比を正しくすると以下のようになります。

![log.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264452/96575784-7cb0-d2da-59a8-039fd1447b5c.jpeg)

いい感じにカラー画像が得られていますね。
実際の本の表紙と見比べたりすると多少の色の違いはありますが、人差し指に乗る大きさのカメラの画質で、しかも色の調整をしていないのであれば許容範囲かな、という感じだと思います。

正しい色を取り出そうとすると、ビデオテスト信号とかパラメータ調整沼に沈むことになります。NTSCが「Never Twice Same Color（同じ色は二度と再現できない）」とか言われる所以ですね。もっとも、デコード自体は完全にデジタル処理で行っているので、入力やパラメータが同じであれば同じ結果が得られます。それでもやはり色信号の調整は大変ですが。