トップ

線を引くのなんて簡単だ !?

線を引くのなんて簡単だ !

線を引く、というグラフィックにおける操作は、一見簡単そうです。「(0, 0) から (9, 0) まで、黒い線を引いてください」という課題が出たら、答は簡単この通り。

……これでいいのだ。そうでしょうか。

斜めの線を引いてみる 1

同じ調子で、(0, 0) から (9, 9) まで線を引いてみます。

やはり悪くなさそうです、が、よく見てみると、まず、

線が細い

のではないかという点に気づきます。

個数が同じ、10 個のドットで、長さとしては約 1.4 倍長い線を引いているわけですから、そのぶん線が痩せて見えているわけです。

補正するために、ドットを 4 個追加してみます。

精細度の高い画像なら気にならないでしょうが、ドット絵だとむしろデコボコ感が気になるかもしれません。

もうひとつの問題点として、想像上の理想的な線のある位置が、本来あるはずの場所より若干下側に寄ってしまった、という問題があります。これをもし平等にしようとすると、両側にドットを追加することになって、汚くなってしまうでしょう。

また、この図は私が手で描いているので、理想的な結果が得られていますが、コンピュータのプログラムで線を引く場合は、「良い結果」が得られるアルゴリズムを考えなければいけない、という問題があります。

斜めの線を引いてみる 2

もっと他のバリエーションも考えてみます。角度とか。

こういった点の集合をどのようにコンピュータで生成したら良いのか、ということについては、過去多数研究され、いろいろなアルゴリズムがありますので検索してみるといいでしょう。

しかしその際に、ここで述べているような、斜めの線が痩せるないし太る、ということが問題になるのかそうでないのか、自分が線を引く目的と照らして、注意が必要です。

極端な例として、線そのものが欲しいのではなく、ポリゴンの内側を塗りつぶすための境界線を求めているのであれば、横方向にスキャンして塗りつぶしているのなら、以下の赤で示したような点列で構わないわけです。

太い線はどうだろうか ?

幅 1 の線ばかりではなく、太さのある線を引くことも考え始めると、困ったことはもっと増えます。描くべき「線」は、その描く方向に対して直角の辺を持った矩形(長方形)であるべきか、端が円になったいわゆるオーバルであるべきか、はたまた、線の幅として指定した長さの辺を持つ正方形の移動した軌跡とすべきか(参照: http://www.chokanji.com/developer/doc/btron3/os_spec/dp/figure_func.html#aec )。

(記憶がはっきりしないのだが TeX か、そのフォントの本でクヌース先生が、精細度の高くない 2 値ビットマップ画像の場合、円の軌跡よりも(正?)多角形の軌跡のほうが良い結果が得られる、と書いていたような気もする)

そろそろはっきりしてきたように思いますが、「そもそも我々が描きたい『線』とは何なのか」というのが、実はあいまいだったことが、色々な問題の元だったわけです。

ここで数学の場合を振り返ってみる

「数学では線とは面積のないものである」などという言葉に代表されるように、(ある意味で)プリミティブな線というのは、実は、最初の例にあったような「幅 1 の線」ではなく「幅 0 の線」だったわけです。たまたまコンピュータの画面では、「表現可能な最も細い線」が程々の太さだった、というだけに過ぎません。

(余談ですが PostScript には「出力機器の出力可能な最も細い線の幅」という線の幅を指定する方法があって、それを使っていたために手元の機器ではいい感じの結果になるデータが、印刷業者にある業務用の機器では肉眼で見えない線になってしまった、という笑い話があります)

最初に描いた例を振り返るならば、数学的には「(0, 0)〜(9, 0) の線」ではなく、「(-0.5, -0.5)〜(9.5, 0.5) の矩形」を描いていたわけです。

むしろ、ここで、「線」を、このような数学的に正確な定義にしてしまうことにしましょう。すると、斜めの線などでは、ピクセルの一部のみを塗りつぶさなければならない、ということになりますが、それについては後で考えます。

格子点はどこだ ?

数学の「座標」において、X と Y の両方の軸で整数になる点を「格子点」と言います。

最初に描いたものが、「(-0.5, -0.5)〜(9.5, 0.5) の矩形」である、とするならば、格子点はピクセルの中央にあることになります(下図左)。これには問題があります。

負の空間までを含めて考えた場合、原点が画像のほんとうの端から半ピクセルぶんずれた位置にあることになってしまっています。また、精細度の異なる環境に図形を拡大したり縮小して持っていった場合、原点のずれがピクセルのサイズに依存しているため、図形もそれに合わせてずれることになります。

つまり、原点をはじめとする格子点は、ピクセルの中央にあるのではなく、ピクセルの角が集まった点や、画像のほんとうの端にある、というモデル(下図右)のほうが、正しい(少なくとも、そのほうが色々都合が良い)わけです。

(座標系については、この他に Y 軸の正負の向きを上下どちらにするか、という議論もあるが、本稿では触れません)

SVG や HTML5 Canvas などの描画は全て、このような、ピクセルの角に格子点がある、というモデルになっています。SVG で幅 1 ピクセルのにじみのない線を描くには、座標に 0.5 を足す、というノウハウがありますが、なんでそういうことになるかというと、このような理由があったわけです。

また、このように、ピクセルの間に格子点があるモデルだとすれば、たとえば「(10, 10)〜(20, 20) の正方形を、線幅ゼロで、中を塗りつぶす」というような指示で、素直に 10x10 の正方形が描けるわけで、「ここで取り扱う長方形は、実際には右と下の辺を含まない領域であり、以下に示す性質を持つことに注意が必要である。 (このように右と下の辺を含まない性質を長方形の half-open property と呼ぶ。)」などといったややこしい概念をわざわざ作らなくてもいいわけです(参考: http://www.chokanji.com/developer/doc/btron3/os_spec/dp/basic_concept.html#acy )。

矩形描画 API に、対角線をなす頂点を指示するのではなく、左上の座標と、幅と高さで、矩形を指定するものがあるのは、頂点で指示した場合に、含まれるピクセルのペアで指定すると「(0, 0)〜(9, 9)」で大きさ 10 の正方形になる、という不自然さも一因かもしれません。

αのためにはγも必要

数学的に正確に、矩形(ないしオーバル)を描くことを、「線を描く」ということにすべきということは、はっきりしました。しかし、特に細かい図形を描く際には、ピクセルが一定の面積を持った正方形だということが問題になります。

そこで、最近のグラフィックシステムでは、いわゆるアルファブレンディングによって、元の色に、上書きする色を混ぜ合わせることで、図形が 1 個のピクセルに部分的に乗っている、という描画をおこなっています(いわゆるサブピクセルレンダリングについては、本稿では触れません)。

ここでは (1, 1) の点から、右下斜め 45 度に太さ 1 の直線を描く、という例を考えます。

このとき、概念としては、次の図で示すような直線を描きたいわけです。

(端点は、ここでは SVG などにおける属性値 stroke-linecap="butt" のものと同様としています)

計算すると、中央のピクセルでは √2 - 1/2 ≒ 91.4% が黒で、残りの 3/2 - √2 ≒ 8.58% が白ですから、その割合のグレーに塗れば良い、ということになります。脇の、黒い部分が 3 角型になっているピクセルはちょうど 25% が黒で、75% が白です。この例では計算で求めましたが、幾何的に計算できない形状などの場合には、仮想的にピクセルを 256 分割してその各ピクセルの色を合計したりします。

ここで 8.58% が白だからといって、255 * 0.0858 ≒ 22 すなわち 16 進表現の RRGGBB で #161616 にすればいいのか、というと、そういうわけにはいきません。

なぜかというと、元々はブラウン管の特性によるものだったのですが、輝度信号の大きさと実際の明るさは、直線的には対応していないのが普通だからです。一般に、画像データなどの数値を I とすると I2 程度の明るさになる、という曲線になります( 0 ≦ I ≦ 1 に正規化すると比較が楽でしょう)。この冪乗する値をガンマ(γ)値といい、たとえばテレビの NTSC 規格では γ = 2.2 としています。γ = 1 だと、直線的に対応している、ということになります。

再現性が重要な画像データでは、必ずこのガンマ値の情報を添えて、そのデータがどのようなガンマ値で再生されることを想定したデータであるかを示し、必要ならば換算し直して表示します。

50% グレーが、どう表示されるか試してみましょう。左半分がハッチングによる 50% グレー、右半分がベタ塗りによる 50% グレーです(ハッチングが横方向の直線状なのは、アナログ系の高周波特性の影響を避けるためです)。

あなたの利用している画像表示系が、PNG の gAMA チャンクによるガンマの指定に対応していれば、それぞれ違って見えるはずです。

(なお、「ガンマ値」として、前述の γ の値ではなく 1/γ の値で指定するものもあるので要注意。このデータの作成に使った netpbm の pnmtopng がそうだった。普通、逆特性のシステムはまずないので、2 前後の値であるか、0.5 前後の値であるかで、どちらなのか通常は判断できる)

ガンマ指定なし、#7f7f7f グレー

ガンマ指定 1.0、#7f7f7f グレー

ガンマ指定 (1.0/0.45)≒2.2、#7f7f7f グレー

ガンマ指定なし、#bbbbbb グレー

ガンマ指定 1.0、#bbbbbb グレー

ガンマ指定 (1.0/0.45)≒2.2、#bbbbbb グレー

最悪の場合に他もつきあうべきか ?

以上のようにアルファブレンディングによって直線を描画した場合、水平ないし垂直な線で、にじみが最悪の結果になるのは、ちょうどピクセルとピクセルの中間に幅 1 の線を描こうとした場合です。幅 2 の 50% グレーの線になってしまいます。

ならば、ということで、逆に、きっちり描画可能な線やエッジも、あえてボカして描く、ということが考えられます。幅 1 ピクセルの線の場合、中心を通るピクセルは同様に 50%、両隣は 50% の線が半分ずつ乗っていると考えられるわけですから 25% ということになります。一種のアンチエイリアス処理(ローパスフィルタ)とも言えます。

何か所かで BTRON の仕様に関して言及していますが、当時としては仕方なかった仕様ではあると思います。