3Dモデルを圧縮する技術2:rANSとは

2025-11-21

3Dモデルを圧縮する技術2:rANSとは

こんにちは、Eukaryaの矢所です。

今回は3Dモデル圧縮技術に関する連載記事の第2回として、rANSについて取り上げます。

ranged Asymmetric Numerical System

、略してrANSは、エントロピー符号化と呼ばれる文字の確率分布を利用して文字列を圧縮する圧縮アルゴリズムの一種です。

他のエントロピー符号化と比較して、rANSはその速さと圧縮率の高さで知られています。具体的には、ハフマン符号化と同等の速度を実現しつつ、与えられた確率分布に対して最大の圧縮率を達成できることが保証されています。

rANSはポーランドの計算機科学者J. Dudaによって、Asymmetric Numerical Systems(ANS)と呼ばれる新しいエントロピー符号化族に関する一連の論文の一部として2013年に発表されました。rANSは、その計算効率のよさと数学的に保証された最適性から、またたく間に世界で最も広く使用される圧縮アルゴリズムの一つとなりました。

実際にrANSが使われている例としては、例えば以下のようなものがあります。

  1. Draco(Googleによる3Dモデルコーデック)
  2. JPEG XL、AV1(画像/動画コーデック)
  3. Opus(音声コーデック)
  4. ZstdおよびLZFSE(MetaおよびAppleによる汎用圧縮アルゴリズム)

また、いくつかのオペレーティングシステムやブラウザなどの基盤システムにも組み込まれていることでも知られており、その普及範囲はもはや計り知れません。まさに今、この記事を表示しているデバイス上でもrANSが動作しているかもしれないーーと言っても過言ではないほど、実はrANSはとても身近なアルゴリズムなのです。

rANSは「3Dモデル圧縮専用技術」というわけではなく、より汎用的な圧縮技術ですが、今回3Dモデル圧縮アルゴリズムの連載にこれを含めているのは、3D圧縮アルゴリズムがrANSコーデックを頻繁に活用しているからです。

例えば、連載の第1回では、メッシュの接続性を特殊な文字列に変換するアルゴリズムであるEdgebreakerアルゴリズムを見てきました。rANSは文字列を圧縮できるため、得られた文字列をrANSコーダーに入力することで、より高い圧縮率を実現できます。さらに、rANSは頂点座標圧縮やテクスチャ座標圧縮に至るまで、幅広く活用されています。

本記事では、rANSアルゴリズムの基礎を解説していきます。

💡 Eukaryaでは、GoogleのDraco 3Dモデル圧縮ライブラリをRustで書き直したdraco-oxideを開発しています。もし興味があればぜひチェックしてみてください!

rANSアルゴリズム

設定と記法

まず、有限な文字の集合SSを考えて、SS上に<で示される順序を固定します(例えば、SSが英語の小文字アルファベットであればa<b<…<zなど)。さらにこれらのアルファベットの確率分布p:S[0,1]p: S \to [0,1]が与えられているとしましょう。確率分布ですので、sSp(s)=1\sum_{s \in S} p(s)=1を満たします。

しかし、rANSが動作するは整数上ですので、固定された正の整数MM

に対して、離散確率分布と呼ばれる関数

P:SNP:S \to \Nを次の2つの条件を満たすものとして定義します。
  1. sSP(s)=M\sum_{s \in S} P(s) = M
  2. sSs \in SについてP(s)p(s)MP(s) \sim p(s)M

さらに、C(s)=tS:t<sP(t)C(s) = \sum_{t\in S:t < s} P(t)と定義します。C(s)C(s)ss範囲へのオフセットと呼ぶことにしましょう。これらの設定は、00からM1M-1までの整数をS|S|個の範囲へと分けるという目的があります。

イメージで理解したい方には、図1が参考になると思います。

図1: 設定の例。S={a,b,c,d} (この順番)のアルファベットに対してP (b)=3、…といった様子で離散確率分布が定められている。つまり、例えば文字列中のとある位置にaが現れる確率はP(a) / M * 100 = 40%と定義されている。オフセットと範囲も示されている。これより、 aの範囲は0から4まで, bの範囲は4から7までといったふうに定義されている。
図1: 設定の例。S={a,b,c,d} (この順番)のアルファベットに対してP (b)=3、…といった様子で離散確率分布が定められている。つまり、例えば文字列中のとある位置にaが現れる確率はP(a) / M * 100 = 40%と定義されている。オフセットと範囲も示されている。これより、 aの範囲は0から4まで, bの範囲は4から7までといったふうに定義されている。

圧縮

それでは、実際に圧縮の方法について見ていきます。SS中の文字でできた文字列(s1,s2,,sm)(s_1, s_2, \cdots, s_m)mmは整数)を符号化したいとしましょう。まず、初期状態X0=0X_0=0と置きます。rANSはこの状態を再帰的に更新することでX1,X2,X_1, X_2, \cdotsと順に計算していき、最終的にXmX_mを計算します。そのXmX_mこそがrANSの出力、つまり圧縮データになります。

各再帰ステップの動作は次の通りです。符号化ステップを表す関数をE:Z×SZE:\Z\times S \to \Zとすると、符号化ステップは各時刻i{1,,m}i \in \{1,\cdots, m\}についてXi=E(Xi1,si)X_i = E(X_{i-1},s_i)と書くことができます(この記事の中では、sSs\in Sが固定されている場合、便宜上Xi=Es(Xi1)X_i = E_s(X_{i-1})と書くこともあります)。

まず、Xi1X_{i-1}P(si)P(s_i)でユークリッド除算した商qqと余りrrを計算します。つまり、次を満たす正の整数qqr<P(si)r<P(s_i)を求めます。

Xi1=P(si)q+r.\begin{align} X_{i-1} = P(s_i) q + r. \end{align}

次にXiX_iを以下の式で計算します。

Xi=E(Xi1,s)=qM+C(si)+r\begin{align} X_i = E(X_{i-1},s) = q M + C(s_i) + r \end{align}

圧縮のステップは以上です。とても単純ですが、何が起こっているかはわかりにくいので、少し解説をします。

qqは前の状態Xi1X_{i-1}の圧縮版を表しています。qqMMをかけているのは、その後に足される数のためにスペースを作るためです。

このことは整数をMM進法で表現するとわかりやすいです。MM進法の数にMMを掛けると各桁が1つずつ左にずれて、最下位の位置に0が挿入されるのがわかると思いますが、こうしてできた数に集合{0,,M1}\{0,…,M−1\}の中からひとつ数を選んで足したところで、2桁目以上の数は変わりません。最後に足されているC(si)+rC(s_i) + rは0以上M1M-1以下なので、この条件を満たしているというわけです。

さて、次のC(si)C(s_i)ですが、これはどの範囲を使っているのかを覚えておくために記録されています。どの範囲を使っているかがわかれば、どの文字が符号化されたかも後で思い出すことができますね。rrは(1)の式で使われている通り、解凍時にqqからXi1X_{i-1}を復元するために欠かせない情報ですので、記録されています。

解凍

さて、圧縮によって文字列から1つの巨大な整数XmX_mを作ることに成功しました。しかし、この巨大な整数から一体どのようにして文字列を解凍することができるのでしょうか?この節では解凍の方法を見ていきます。

rANSで符号化されたデータの復号化は、符号化プロセスを逆にたどっていくことによって実現します。これはつまり、最後に符号化された文字が最初に復号化されるということです。

さらに具体的に言えば、ii個の文字を符号化した後の状態値XiX_iが与えられているとき、まずsis_iを復元し、次にXi1X_{i-1}を計算する、というのが各復号化ステップの概要になります。

ここでも必要な記法を導入します。符号化の場合と同様に、復号化関数をD:ZZ×SD:\Z\to\Z\times Sで表します。すなわち、各時刻i{1,,m}i\in\{1,\cdots,m\}についてD(Xi)=(Xi1,si)D(X_i) = (X_{i-1},s_i)と書くことができます。

まずXiX_iMMでユークリッド除算を行います。つまり、次を満たす正の整数QQR<MR<Mを計算すします。

Xi=MQ+RX_i = M Q + R

RRMMより小さい正の整数であるため、RRを含む範囲を持つ文字siSs_i\in Sが存在することがいえます。すなわち、

si=max{s:C(s)R}s_{i} = \max \left\{s : C(s) \leq R \right \}

という計算をすることでsis_iを復元することができます。

ちなみにこの計算は、{0,1,,M1}\{0,1,\cdots,M-1\}の各値とそれを含む範囲を持つ文字の表を先に計算しておくことで効率的に実装できます。

sis_iが特定できたら、残りはXi1X_{i-1}を計算するのみです。まず、次の等式が成り立つことが解ると思います。

R=C(si)+rQ=q\begin{align} R &= C(s_i)+r \\ Q &= q \end{align}

ここでqqrrは前の節の符号化関数EEで定義された値です。等式(3)と(4)を使って等式(1)の変数qqrrを書き換えると、最終的な復号化の式が得られますね。

Xi1=P(si)Q+RC(si).X_{i-1} = P(s_i)Q + R - C(s_i).

以上が復号化の1ステップです。復号化のプロセスは、最終的にXi=0X_i = 0に到達するまで、繰り返し続けられます。そのたびにsis_iを記録し、最終的に得られる文字列が完全に復号化された文字列になります。

ここで、復号化は符号化とは逆の順序で進むことに注意が必要です。つまり、(s1,s2,...,sm)(s_1, s_2, ..., s_m)をこの順序で符号化した場合、シンボルは(sm,sm1,,s1)(s_m,s_{m-1},\cdots,s_1)の順序で復号化されるということです。

では、簡単な例題を見ていきましょう。

ここでは、図1の設定をそのまま用います。現在までにX5=691X_{5}=691まで符号化されているとして、ここで6番目の文字として「c」を圧縮してみます。

上記の計算に従うと、P(c)=2P(c) = 2q=345q = 345r=1r= 1となることがわかると思います。つまり、X6=qM+C(c)+r=3458X_6=qM+C(c)+r=3458が得られます。これだけで圧縮は完了です。

では、X6X_6からX5X_5と文字「c」をどのように復号化できるでしょうか?

X6X_6MMで割ると商Q=q=345Q=q=345と余りR=8R=8を得ます。RRから文字と余りrrを復号化するには、図2を参照してください。C(c)R<C(d)C(c) \leq R < C(d)(具体的には78<97\leq 8 < 9)であるため、RRは文字「cc」の範囲内にあるので、ここで「cc」を復号化できます。

また、RRは「c」の範囲内で値11を持つことがわかる(図のオレンジの部分)ので、r=1r=1であることがわかりますね。したがって、X5=P(c)q+r=691X_{5}=P(c) q + r=691が得られるというわけです。

図2: 上記の例題での圧縮と解凍のうち、範囲の計算の様子。
図2: 上記の例題での圧縮と解凍のうち、範囲の計算の様子。

ストリーミングrANSアルゴリズム

前の節では、文字列に関するすべての情報を含む巨大な整数を作成することを主なアイデアとするrANSコーデックを紹介しました。

rANSは与えられた確率分布に対して最適なエントロピー符号化を実現することで知られていますが、文字列のサイズが大きくなるにつれて急速に実用的でなくなってしまいます。

というのも、ますます大きくなる整数に対してユークリッド除算を実行する必要があり、ユークリッド除算は整数が何百桁、何千桁と大きくなれば計算がほぼ不可能になってしまうのです。

この問題を解決策として、各状態の値XiX_iを分解して、適度に小さく保つということが考えられます。これは、ストリーミングrANSとよばれる、rANSに少し変更を加えたアルゴリズムによって実現することができます。

この節ではストリーミングrANSについて解説していきます。

ストリーミング符号化

まず、正の整数kkllを選択します。この選択は完全に自由ですが、選択するにあたってはいくつかの指標があります。

まずkkは、ストリームに送信する各転送のビット数を示しています。最も単純な実装ではk=1k=1に設定し、バイトストリームの場合はk=8k=8に設定するのが良いでしょう。

llXiX_iをどれだけ小さく保つかを示すアルゴリズムのパラメータであり、llの値が高いほどアルゴリズムは遅くなりますが、圧縮効率は向上します。

ここで解説するストリームrANSは、各ステップにおいて状態の値がlMlM2klM12^klM-1の間にあることを保証してくれるアルゴリズムです。つまり、

I={lM,,2klM1}I=\{lM,\cdots,2^k l M-1\}

と定義した場合、常にXiIX_i \in Iとなることを保証してくれます。例えば、64-bitの非負整数を用いる場合、2klM2^klM2642^{64}を超えないようにrANSを設計することは実装においてひとつの重要なポイントであると言えます。

ストリーミングrANSの考え方自体は非常に単純で、各ステップiiにおいて、E(Xi1,s)E(X_{i-1},s)が範囲IIから外れそうになるときは状態Xi1X_{i-1}2k2^kで割り、余りのkk数をストリームに出力してしまうことで、XiX_iを常にIIの内部に保っています。

さて、状態の値がIIの範囲から外れようとしていることをそもそもどのようにして知ることができるのでしょうか?言い換えれば、どの状態値XXと文字sSs \in SのときE(X,s)IE(X,s) \in Iとなるのでしょうか?この問に答えるには、次のように定義される集合

Is=Es1(I)I_s=E_s^{-1}(I)

について詳しく知る必要があります。

この集合を計算するのに役立つ事実として、関数EsE_sが単調増加であることがあげられます。つまり、入力が大きくなればなるほど、出力も大きくなるという性質を持っているのです(これは簡単に確かめることができます)。

この事実さえあれば、Ls=min{L:Es(L)lM}L_s = \min \{L: E_s(L)\geq lM \}およびHs=max{H:Es(H)2klM1}H_s = \max\{H:E_s(H)\leq2^klM-1\}の2つの数さえわかればもう上記の問は解決します。なぜなら、単調性により、LsXHsL_s \leq X \leq H_sを持つ任意の数XXIs I_sに含まれることがわかるからです。

では、LsL_sHsH_sの実際の値は何でしょうか?もしお時間があれば、とてもいい演習になると思いますので、ぜひご自身で計算してみてください!

… どうでしたでしょうか?それでは答え合わせをしていきます。結果は以下のようになります。

Is={Ls,,Hs}={lP(s),,2klP(s)1}.I_s=\{L_s,\cdots, H_s\} = \{lP(s),\cdots,2^k lP(s)-1\}.

IsI_sに対するこのすっきりとした式になるのは、実はIIはそのように設計されているおかげなのです!

したがって、ssを符号化しようとするときに状態値が上記のIsI_sの内部にあれば、結果として得られる状態値XiX_iは決してオーバーフローしないことがわかりましたね。

これでいよいよアルゴリズムを説明する準備が整いました。状態Xi1X_{i-1}が与えられたとき、シンボルsis_iは次のように符号化することができます。

while Xi1∉Isi:        output Xi1mod2k        Xi1Xi1/2kXi=E(si,Xi1)\begin{align} &\text{while } X_{i-1} \not \in I_{s_i} : \\ &\;\;\;\;\text{output } X_{i-1} \mod 2^k \\ &\;\;\;\;X_{i-1} \leftarrow \lfloor X_{i-1} / 2^k \rfloor \\ &X_i = E(s_i,X_{i-1}) \end{align}

残る疑問はただ一つです。whileループが有限のステップで終了することをどのように担保できるのでしょうか?2k2^kによる除算がIsiI_{s_i}を飛び越えてしまう場合は無限ループになってしまいますが、その心配はないのでしょうか?

実は、2k2^kによる除算は区間IsiI_{s_i}を飛び越ることは決してありません。これも、IIの設計のおかげです。もし区間IsiI_{s_i}を飛び越える状態値が存在するとするならば、Isi{X,,2kX}I_{s_i}\subsetneq\{X,\cdots,2^kX\}を満たす整数XXが存在しなければならなくなります。これはlP(si)<XlP(si)lP(s_i)<X \leq lP(s_i)またはlP(si)X<lP(si)lP(s_i) \leq X < lP(s_i)を意味しますが、どちらの場合も決して起こり得ないというのは自明なことですよね。これは矛盾となりますから、区間IsiI_{s_i}を飛び越えるかもしれないという前述の心配は杞憂であったというわけです。

ストリーミング復号化

普通のrANS同様、復号化は符号化の逆の操作によって行われます。各時刻i{1,,m}i\in\{1,\cdots,m\}において、前の節で説明した普通のrANS復号化プロセスを使用して、XiX_iから文字sis_iXi1X_{i-1}を復号化することから始めます。

文字を復号化した後、状態Xi1X_{i-1}IIの下限kMkMを下回る場合、符号化されたビットストリームから追加のkkビットを読み込んで2kXi12^kX_{i-1}に加え、それを新たにXi1X_{i-1}とします。つまり、以下のようにアルゴリズムを定義できます。

(Xi1,s)=D(Xi)while Xi1<kM:        Xi1Xi12n+next k bits from bitstream\begin{align} &(X_{i-1},s) = D(X_i)\\ &\text{while } X_{i-1} < kM: \\ &\;\;\;\;X_{i-1} \leftarrow X_{i-1} \cdot 2^n + \text{next $k$ bits from bitstream} \end{align}

しかしここでも、符号化中に行われるkkビットの読み取りの回数が復号化中の回数と異なり、復号化が失敗するのことがあるのではないかと疑問に思うかもしれません。

特に、XiIX_i \in Iとなったらkkビットの読み取りを停止しますが、これは早すぎるということは起きないのでしょうか?もしかしたら、2kXiI2^kX_i\in Iでもある可能性はないでしょうか?

実は、これもIIの設計により、XX2kX2^k Xの両方がIIに含まれるような整数XXは存在しないことが保証されています(この事実は上記同様簡単に確かめられるので、演習にご活用ください!)。

これにより、ストリームの読み取り回数が一意に決定されることが保証され、アルゴリズムは前述のような曖昧な状況には遭遇しないのです。

以上です!これで実用的なrANSアルゴリズムが完成しました。

tANSアルゴリズムの概要

最後に、非常に短くではありますが、tANSというrANSと非常に関係の深いアルゴリスムを紹介します。

前の節では、各時刻iiにおいて状態値XiX_iを特定の範囲に制限するrANSのストリーミング版を見てきました。E(X,s)E(X,s)は一対一の写像(単射)であるため、rANS符号化プロセス全体を通してE(X,s)E(X,s)に供給される入力は有限個しかありません。

より具体的には、ストリーミングrANSでは、sSs \in SX{lP(s),,2klP(s)1}X \in \{lP(s),\cdots,2^klP(s)-1\}に対してE(X,s)E(X,s)を定義すれば十分であり、このようなペア(X,s)(X,s)は有限個しかないということです。

tANSアルゴリズム(tabled Asymmetric Numerical System)は、符号化/復号化プロセスの開始時にE(X,s)E(X,s)の表を作成することでrANSの速度を向上させる、ANS族の一種です。この表により、ユークリッド除算や整数乗算を含むE(X,s)E(X,s)の計算を、ただの表の参照にまで削減できます。

このアルゴリズムの課題は表の作成方法にあります。E(X,s)E(X,s)のすべての値を計算することもできますが、これではEEの計算を避けることを目的とするtANSの意義が失われてしまいます。

Jarek Dudaの論文[1]は、これらの表の値を明示的に計算することなくE(X,s)E(X,s)の表を作成する方法を示していますが、その方法を詳しく説明していたら本記事の範囲を大きく超えてしまいますので、今回はこのあたりで御免を蒙ります。

結論

本記事では、あらゆる圧縮の場面において強力なツールとして使われているrANSとそのストリーミング版について紹介いたしました。

基本的なrANSコーダーは文字の出現確率に基づく最適なエントロピー符号化を実現しますが、そのままでは状態値のサイズが増大することによる実用上の制限に直面します。この問題は、多少の圧縮率とのトレードオフはあるものの、ストリーミングrANSという状態値を制限する仕組みを導入することで完全な解決を見ました。

また、圧縮率と計算複雑性の間で異なるトレードオフを提供するANS族の一種であるtANSについても簡単に触れました。

これらの近代エントロピー符号化技術は、3Dモデル圧縮のみならず、さまざまな場面で現代のIT技術を影から支えている、まさに縁の下の力持ちと言ってよいと思います。本稿が、少しでも多くの方にとって、普段陽の当たらない彼らを知るきっかけになれば幸いです。

では、今回はこれにて。

参照

  1. Duda, J. (2013). Asymmetric numeral systems: entropy coding combining speed of Huffman coding with compression rate of arithmetic coding. arXiv preprint arXiv:1311.2540. https://arxiv.org/pdf/1311.2540
Japanese

Eukaryaでは様々な職種で採用を行っています!OSSにコントリビュートしていただける皆様からの応募をお待ちしております!

Eukarya 採用ページ

Eukarya is hiring for various positions! We are looking forward to your application from everyone who can contribute to OSS!

Eukarya Careers

Eukaryaは、Re:Earthと呼ばれるWebGISのSaaSの開発運営・研究開発を行っています。Web上で3Dを含むGIS(地図アプリの公開、データ管理、データ変換等)に関するあらゆる業務を完結できることを目指しています。ソースコードはほとんどOSSとしてGitHubで公開されています。

Re:Earth / ➔ Eukarya / ➔ note / ➔ GitHub

Eukarya is developing and operating a WebGIS SaaS called Re:Earth. We aim to complete all GIS-related tasks including 3D (such as publishing map applications, data management, and data conversion) on the web. Most of the source code is published on GitHub as OSS.

Re:Earth / ➔ Eukarya / ➔ Medium / ➔ GitHub