はじめに
これは少し変わった技術記事です。紹介する技術そのものは1980年代前半のものですが、実際にPythonで動かして検証したのはつい最近、しかも実装作業の大部分をAI(Claude)と一緒に行いました。
私は40年ほど前、画像信号の帯域圧縮符号化——今で言う画像・映像圧縮——の研究をしていました。当時いくつかの論文は電子情報通信学会論文誌などに掲載されたのですが、それらをまとめて博士論文にする作業は、仕事の忙しさに追われるうちに完了しないまま時間だけが過ぎてしまいました。
今回、AIの力を借りてその博士論文をほぼ完成させることができました。せっかくなので、当時の技術がどんなものだったか、そして令和の今それをPythonで再現するとどうなるかを紹介したいと思います。
当時の時代背景
1980年代前半は、今のようにJPEGもMPEGも存在しない時代です。動画像を安価に伝送・蓄積するには、画素をそのまま送るのではなく「情報を間引いても人間の目にはあまり劣化がわからない」性質を利用した帯域圧縮符号化が重要な研究テーマでした。
当時研究されていた代表的な手法は大きく2つに分けられます。
• 予測符号化(DPCM): 隣接画素との差分だけを伝送する方式。回路は簡単だが、圧縮率には限界がある。
• 直交変換符号化: 画像をブロックに分けて周波数成分のようなものに変換し、電力の小さい成分を粗く量子化する方式。フーリエ変換、アダマール変換、KL変換(カルーネン・ルーベ変換)などが候補になる。
この研究では、この2つを組み合わせた「差分-アダマール変換符号化」という方式を提案しました。アダマール変換を選んだ理由は単純で、変換行列の要素が+1と-1しかないため、乗算を使わず加減算だけで実現できる、つまり当時のハードウェアでも実時間処理がしやすいという点です。
差分-アダマール変換符号化の考え方
まず画面の水平方向に8画素ずつのブロックを取り、アダマール変換をかけます。N次のアダマール行列は次の漸化式で作れます。
python
def hadamard_matrix(n: int) -> np.ndarray:
"""n(2のべき乗)次のアダマール行列を再帰的に生成する。"""
if n == 1:
return np.array([[1.0]])
h_half = hadamard_matrix(n // 2)
top = np.hstack([h_half, h_half])
bottom = np.hstack([h_half, -h_half])
return np.vstack([top, bottom])
この変換出力(各成分をsequency成分と呼びます)は、低い成分に電力が集中し、高い成分ほど電力が小さくなる性質があります。そこで各sequency成分ごとに、1ライン前の同じ成分との差分を取り(垂直方向のDPCM)、その予測誤差だけを符号化します。直交変換と予測符号化のいいとこ取りをしたような方式です。
この非適応な方式だけでも、代表的な画像に対して2.6〜3 bit/pel程度まで圧縮できることを当時確認していました(1画素8bitが原信号なので、3〜4倍弱の圧縮です)。
適応化 ―― モードI〜IVによる4段階の符号化
固定のビット配分では、画像の輪郭部分のように予測誤差が大きいブロックに合わせてビットを配分するしかなく、平坦な部分ではビットが余ってしまいます。そこで、ブロックごとの予測誤差の大きさを見て、次の4段階(モードI〜IV)のどれかを動的に選ぶ適応化を行いました。
• モードI: 予測誤差がどのしきい値も超えない ―― 何も符号化しない(前の値をそのまま使う)
• モードII: 低域成分がしきい値を超える ―― 低域のみ符号化
• モードIII: 中域成分がしきい値を超える ―― 中低域を符号化
• モードIV: 高域成分がしきい値を超える、または輪郭部特有の分布が見られる ―― 全成分を符号化
Pythonで書くと、モード判定はこんな形になります(eが予測誤差、t_low・t_highがしきい値)。
python
def decide_modes_batch(e: np.ndarray, t_low: float, t_high: float) -> np.ndarray:
ae = np.abs(e)
high = ae[:, N // 2:] # 高域成分
mid = ae[:, N // 4:N // 2] # 中域成分
low = ae[:, :N // 4] # 低域成分
cond1a = (high > t_high).any(axis=1)
cond1b = (high > t_low).sum(axis=1) >= (N // 4) # 輪郭部検出のための条件
cond1 = cond1a | cond1b
cond2 = (mid > t_low).any(axis=1)
cond3 = (low > t_low).any(axis=1)
modes = np.ones(e.shape[0], dtype=np.int64) # 既定: モードI
modes[cond3] = 2 # モードII
modes[cond2] = 3 # モードIII
modes[cond1] = 4 # モードIV(最優先)
return modes
条件1のところに「高域成分のうち一定数以上がしきい値を超えたら」という判定が入っているのがポイントで、これは輪郭部(エッジ)を検出して優先的に高画質化するための工夫でした。今で言うところの、画像のエッジ部分にビットを多く配分するレート制御の考え方に近いものです。
この適応化によって、代表的な画像に対して平均1.28〜1.43 bit/pelで十分満足できる画質、1.01〜1.11 bit/pelでも許容できる画質が得られる、という結果を当時得ていました。固定配分の方式に比べて1 bit/pel以上の改善です。
カラー画像への拡張
モノクロームでの検討のあと、カラー画像への拡張も行いました。RGBをそのまま3枚符号化するのではなく、人間の視覚特性を利用してYIQ色空間(輝度Yと2つの色差信号I, Q)に変換し、Y信号は全ライン、I信号は奇数ラインのみ、Q信号は偶数ラインのみを符号化する「モード分離方式」を採用しました。符号化しなかったラインは前後のラインの平均値で補間します。
python
_RGB2YIQ = np.array([
[0.299, 0.587, 0.114],
[0.596, -0.274, -0.322],
[0.211, -0.523, 0.312],
])
この方式によって、代表的なカラー画像に対して平均2.16 bit/pelで十分満足な画質、1.72 bit/pelで許容できる画質が得られること、また伝送路の符号誤り率が10⁻⁴以下であればほとんど画質劣化がないことなどを確認していました。
AIと一緒に「動くコード」にしてみる
今回、この一連の方式をPythonで実装し、Webカメラの映像をリアルタイムに符号化→復号して原画像と並べて表示するデモを作りました。実際の可変長符号のビットストリームまでは作らず、量子化雑音を含めてエンコード→デコードした結果だけを再現する、圧縮効果検証用のシミュレータという位置づけです。
この作業で特に助かったのは、次の2点でした。
- 古い文書の読み取り: 40年以上前にワープロ専用機やOffice文書として作られた原稿はOCRの誤変換や文字化けが多く、数式もかなり崩れていました。これをAIと一緒に1章ずつ照合しながら修正していきました。
- 図からのアルゴリズム抽出: モード決定アルゴリズムの正確な条件分岐は、本文の説明文だけでは細部まで再現しきれず、AIに論文中のフローチャート画像を直接読み取ってもらうことで、輪郭部検出の条件式まで含めて正確に実装できました。
出来上がったデモでは、画面上のトラックバーでしきい値(t_low, t_high)やY・I・Qそれぞれのビット配分を実行中に変更でき、bit/pelとPSNR(画質指標)がリアルタイムに変化する様子を目で確認できます。40年前は専用のハードウェア(ミニコンとICメモリを組んだ実験装置)でしか確認できなかったことが、今は手元のノートPC1台とWebカメラだけで、しかもGUIで対話的に確認できるようになったわけで、ここに時代の変化を感じます。
今の動画圧縮とのつながり
差分-アダマール変換符号化の考え方――「直交変換で電力を集中させ、ブロックごとに適応的にビットを配分し、時間・空間方向の予測を組み合わせる」――は、その後JPEGやMPEG、H.26x系のコーデックが採用した「変換符号化(DCTなど)+適応量子化+動き補償予測」という枠組みの原型のひとつと言えます。アダマール変換そのものはコサイン変換(DCT)に主役を譲りましたが、加減算だけで実現できる直交変換として、今でもビデオコーデックの一部の変換モードや、ハードウェア制約の厳しい組み込み用途では使われ続けています。
おわりに
40年前に途中で止まってしまった研究を、当時とは全く違う形――Pythonとリアルタイムのカメラデモという形で、もう一度自分の手で動かせたことは、ちょっとした感慨がありました。当時は「発表できないまま終わった研究」でしたが、AIという新しい道具のおかげで、少なくとも自分自身が納得できる形にまとめ直すことができました。
古い研究ノートや未完成の論文を持っている方が、それを「今の技術で動かしてみる」きっ
かけになれば嬉しく思います。