どうも、地球です。
この記事は専科雑談アドベントカレンダー、#パソコン カテゴリの記事です。
今回は画像について個人的に色々と調べた事を紹介したいと思います。主にjpegとpngについてで、おまけとしてgifとsvgの話もします。日常的にtwitterやdiscordに流れてくる画像データで網膜を焼いていらっしゃる皆さんには身近な話でしょうし、これからはjpegの画像が流れてくるたびに「あ~離散コサイン変換した結果を量子化してランレングス符号とハフマン符号で圧縮しているんだな~」と感じられればインターネットがもっと楽しくなる事間違い無しですね。
jpeg
jpegとは"Joint Photographic Experts Group"の略称で、世界で最も広く使われている画像フォーマットの一つで、皆さんも普段使われている事でしょう。jpegには以下のような性質があります。こちらも恐らく皆さんご存じの事でしょう。
- 画像サイズを極めて小さくすることが出来る。
- 圧縮率を高くすると画像が劣化する。
1番の画像サイズを小さくできる、という性質は非常に強力です。例えば以下の2枚の画像を比べてみます。1枚目がjpeg、 2枚目がpngです。
見た目にはほとんど差がありませんね。一方で画像サイズについてはどうでしょうか?
jpegが56.3kb, pngが441kbと約8倍もの差がついています。画像の内容によって圧縮のしやすさに差がありますので、絶対にこれだけの差が出るわけではありませんが、見た目にほとんど変化を与えずに大きく画像を圧縮できる優秀なフォーマットであることが伺えます。そのため、表示速度が重要となるWebページ等ではjpegが良く使われます。
一方で2番の圧縮率を高くすると画像が劣化する、という特性はjpegを使用する上で大きなデメリットとなります。インターネットが好きで好きでしゃーない皆さんなら、何度も擦られて保存と投稿を繰り返した挙句どこかで圧縮率を上げられてボロボロに劣化したネットミーム画像をご覧になったことがあるでしょう。
これらの画像はPILを使用してjpegのクオリティを100, 50, 10, 1と順に落としていったものです。
え!?なんかクオリティ1の画像意外と良くない!?ドット絵みたいで何かかっけー...
圧縮率と言う言葉を何度か使いましたが、この言葉はあまり適切ではないかもしれませんね。jpegが画像をどの程度小さくできるかは画像の内容にもよるので、必ず一定の割合に小さくできるというものではありません。画像編集ソフトなどではクオリティ、品質という言葉が良く使われます。
さて、このように一長一短なjpegですが、長所と短所は表裏一体なもので、クオリティを下げれば下げるほど画像の容量は小さくなります。圧縮率という言葉を使うのであれば、低品質=高圧縮率と言えるでしょう。
これらはクオリティ100, 50, 10, 1の各画像の容量です。クオリティを下げると容量がその分小さくなっている事が分かります。
ここでは、jpegの圧縮方式について簡単に説明します。
概要
jpegは大まかに以下のような流れで画像の圧縮を行います。
- 画像をYCbCr形式(後述)に変換する。
- Cb,Cr成分の解像度を4分の1にする。
- 画像を8x8ピクセルのブロックに分割し、各ブロック内の細かな特徴を除去する。
つまりjpegの圧縮はCb, Cr成分の情報や画像内の微細な色味の変化等の情報を捨て去ることで画像の容量を小さくしています。ここで捨て去った情報は二度と復元することが出来ないためjpegは非可逆圧縮となっています。一度失ったものは取り戻す事が出来ないんですね~。例えばこういう記事を読んでいる時間とかさ。
説明
さて、概要の各ステップについて説明していきたいと思います。
YCbCr形式への変換
ネットの皆ならRGBは知ってますよね?画像の各ピクセルを赤青緑の光の三原色で表現する形式です。画像を表現する方法は他にも色々ありますが、jpegではYCbCrという方式を取っています。Yはピクセルの明るさ、Cbは青色成分、Crは赤色成分に対応します。 以下の画像はRGB画像をYCbCrに変換した際の各成分の擬似的なイメージです。順番にY, Cb, Cr成分です。
Cb, Cr成分の圧縮
ここで第一の圧縮が行われます。上の画像を見てみてください。白黒画像だけがやたら鮮明に見えませんか?(色弱等の方はもしかしたら違うかもしれません。)人間の眼は色味の変化よりも明るさの変化に敏感です。人間の網膜には桿体と錐体という2種類の細胞が分布しており、色を認識できるのは錐体だけです。錐体と桿体の数には大きな差があり、桿体が約1億2000万個あるのに対し、錐体は約600万~700万個と言われています[1]。
つまり何が言いたいかと言うと、色味成分(Cb, Cr)の情報は明るさ成分に比べてそれほど重要では無いと考えられているということです。人間のこのような特性を利用して、jpeg画像ではCb, Cr成分の解像度を各辺半分ずつにして4分の1に圧縮してしまいます。
Y成分、Cb成分の元の解像度の画像と、4分の1に圧縮した画像を同じ大きさで並べました。
Y成分の画像は若干ボケて見えますが、Cb成分の画像はほとんど変わらないように見えると思います。よく見ればメキシコ湾のあたりがボケてるかな?
これが第一の圧縮です。
離散コサイン変換
続いて第二の圧縮を行います。
高校生の時に「三角関数ってどこで使うんw?」とか言ってた人いますか。jpegで使います。離散コサイン変換というのは、大きさと周波数の異なる縦方向と横方向の波の組み合わせで画像を表現する変換手法です。もっと簡単に言うと以下の64種類の画像[2]の組み合わせで画像を表現します。この各画像を基底関数と言います。
もちろん画像全体に対してこの変換を行うととんでもないガビガビになってしまうので、画像を8x8ピクセルの小さな領域に分割し、各成分の各領域に離散コサイン変換を適用します。つまり8x8の領域の各ピクセルの値を、そのピクセルの明るさを表す値ではなく、64種類の基底関数の重みを表す値に変換するわけです。
なぜこのような事をするのでしょうか?これは概要の3番、「細かな特徴を除外する」を行うためです。基底関数の画像群の右下と左上を見比べてください。右下の画像ほど模様が細かくなっている事が分かります。上の画像は見やすいように引き延ばされているので分かりますが、もし1番右下の画像の格子の1つ1つが1ピクセルの大きさだったとしたら格子模様として見ることが出来るでしょうか?人間の眼には高い周波数成分を認識することが出来ないという特性があります。ここでいう周波数と言うのは色の変化の細かさの事です。
この特性を感じられる身近な例としては、漫画が挙げられます。本棚からモノクロの漫画を取り出してください。ちゃんと取り出した?なんだろ。嘘喰いかな?さて、適当にページを開いて、灰色になっている部分を探してください。その部分に目を近づけてよく見てください。実際に灰色で塗られている訳ではなく細かな黒色の点々で構成されていると思います。
これは、網点というもので、点のサイズや密度を変えることでモノクロ印刷でも見かけ上の色の濃さを変えることが出来ます[3]。
単なる点の集合なのに色が塗られているように見えるというのは、高い周波数成分を認識することが出来ないという人間の眼の特性を利用したものと言えるでしょう。もし人間が完璧に色を見分けることが出来たら、漫画はなんか細かいブツブツに見えるはずです。キショ~。
話がそれましたが、離散コサイン変換により画像内の領域を基底関数に分解する事で、その領域の特徴を細かさの異なる複数の特徴に分ける事が出来るので、その中から細かい特徴だけを取り除くことが出来るようになります。
量子化
離散コサイン変換の結果から、周波数成分の高い基底関数の強さを弱めることで、画像中の領域から細かな特徴を除去します。この工程を量子化と言います。量子化には以下の「量子化テーブル」という事前に決められた8x8の表を使用します。いかれた秘密道具みたいな名前でかっこいいですね!
離散コサイン変換した8x8の領域の各値を量子化テーブルの各値で割ります。一番左上の値は4で割って、一番右下の値は21で割る、といった具合です。割り算の結果は、整数になるように丸めます。例えば左上の値が10だった場合は10÷4=2.5→2になります。1より値が小さくなったらその値は0にします。表を見ると分かりますが、左上の値は小さく、右下の値は大きくなっています。先ほどの基底関数の画像では右下ほど細かな模様になっていましたよね。つまりこのような量子化テーブルを用いることで、高い周波数成分の値ほど弱くすることが出来ます。割り算した後は今度は逆に量子化テーブルの値をかけ算します。こうする事で元の値に近い値に戻すことが出来るのですが、0になった成分だけは0に何をかけても0なので元に戻りません。つまり高周波成分のみを"0"にすることが出来るという事なのですが、これが後々重要になってきます。
最初にjpeg画像のクオリティの話をしましたが、クオリティによって量子化テーブルの内容が決まります。クオリティを小さくすると量子化テーブルの値が大きくなり、よりたくさんの基底関数の大きさが0になります。クオリティ1の画像がドット絵のようになっていたのも、8x8の各領域内で特徴がほとんど失われてしまっていたからです。jpegの画像の劣化は、このように8x8の単位で起こります。
量子化はY,Cb,Crの各成分に対して独立に行うのですが、成分毎に異なる量子化テーブルを使用し、特にCb,Cr成分には、Y成分よりも多くの値が0になりやすいテーブルを使用します。ここでも色味の変化に対して鈍感であるという人間の特性を利用しています。ほんと、鈍感なんだから...///
ランレングス符号化
さて量子化を行いましたが、これって意味があるんでしょうか?だって値が0であっても100であってもデータの大きさは変わらず8ビットのはずです。であれば0にする意味ってなんなのでしょうか?趣味?その答えがランレングス符号化です。
量子化した8x8の領域を次のように並び替えます。
このように並び替える事で、右下に0が集まりやすいという量子化の特性を利用して、8x8の領域を末尾に0が多く並んだ長さ64の列に変換できます。この列にランレングス符号化という変換を行います。ランレングス符号というのは、連続して並ぶ値を、(値, 個数)のペアに変換したものです。例えば上の数列の場合だと、10,1,20,2,30,3,40,4...0,15となります。量子化を行い高い周波数成分を除去したのは、このためです。多くの成分を0にすればするほどまとめることが出来るので、データを圧縮することが出来ます。
ハフマン符号化
ここまでで画像サイズを十分小さくすることが出来ましたが、最後にもう一つ、ハフマン符号化という処理を行います。現時点では一つ一つの値が8ビットで表現されています。しかし、ここまで変換した結果の中には、よく出てくる数値と、あまり出てこない数値があります。例えば"0"はほぼ全ての領域で出てきます。このような特徴を利用して画像サイズを小さくするのがハフマン符号化です。この符号化では、よく出てくる値には短いビット列を割り当てて、代わりにあまり出てこない値には長いビット列を割り当てます。Wikipediaにいい感じの具体例が載っているので見てみてください[4]。
jpegのまとめ
jpegについて、主に圧縮方法に焦点を絞って紹介しました。イラストや写真が好きな方はエッジがはっきりとした画像はjpegに向かないという話を聞いたことがあるかもしれません。それはjpegが色味成分の解像度を落としたり、8x8単位で特徴を削ったりしているためにエッジがぼやけてしまうためです。非常に強力な圧縮率を誇るjpegですが、賢く使って快適に美麗なインターネットライフを送りましょう!
余談
よくjpegは保存を繰り返すと劣化すると言われますよね。あれって本当なんでしょうか?
PILを使用してクオリティ20で1000回の上書き保存を繰り返しました。
1回目の画像と1000回目の画像です。どうでしょう?ほとんど差はありません。同様の事を確認されている方が他にもおられます。[5]
一度圧縮したjpeg画像を再度圧縮する事を考えます。
Cb, Cr成分のサイズの圧縮については、確かに元の画像から4分の1に圧縮すれば細部の特徴が失われます。しかし、一度4分の1に圧縮したものを元の大きさに戻し、それを再度4分の1にしても最初に4分の1にしたものと同じ結果が得られるはずです。
離散コサイン変換、量子化の組み合わせも、一度これらの処理を行った結果に再度同じ処理を行っても、すでに高周波成分は取り除かれており、低周波成分は処理の前後で(初回の小数の丸め誤差の影響を除いて)値が変わらない事から影響を受けないはずです。ランレングス符号とハフマン符号は非可逆だし...jpegの保存を何度繰り返しても、同じクオリティであれば劣化していくという事は無いと思うのですが...
詳しい人は教えてください。
png
さて、jpegの次はpngを見てみましょう。pngはPortable Network Graphicsの略です。こちらもjpgと並びインターネットで最も広く使われているフォーマットの一つです。みなさんも普段から使っていると思います。pngには以下のような性質があります。
- 画像サイズを(jpegほどではないけど)小さくすることが出来る。
- 可逆圧縮なので画像が劣化しない。
このような特徴がありますので、画像サイズよりも品質の方が重要となる用途ではjpegよりもpngが良く使われます。また、jpegと違い、pngは透明度を扱うことが出来ますので、半透明などを扱いたい場合もpngが良く使われます。
ここではpngの圧縮方式について説明します。
概要
zipです。zipで圧縮してます。だから小さくなりますし、完全に元に戻ります。
説明
説明としては概要でもう大体出来ているのですが、もう少し補足をします。pngで使用している圧縮方式はdeflateというものです。しかしdeflateの前に、圧縮の効果を出来るだけ高めるためにフィルタ処理というものを行います。
フィルタ処理
画像の横一列に対して、Sub,Up,Average,Paeth,Noneの処理のいずれか1つを行います[6]。
Sub
あるバイトを、その値と左隣のバイトの差で置き換える。例えば10,11,12,13,14と値が並んでいるところにSubを適用すると、10,1,1,1,1となります。
Up
あるバイトを、その値と上のバイトの差で置き換える。例えばある行で10,10,10,10,10...次の行で11,11,11,11,11...と値が並んでいる時に11,11,11,11,11...の行にUpを適用すると、1,1,1,1,1...となります。
Average
あるバイトを、左隣と真上のバイトの平均値との差で置き換えます。ある行で0,11,15,16... 次の行で5,11,18,22...と値が並んでいる時に5,11,18,22...の行にAverageを適用すると、5,5,5,5...となります。
Paeth
あるバイトを、左隣、真上、左上の3つのバイトのPaeth値との差で置き換えます。Paeth値...?どうやらpngのために作られた?値?みたいな?やつです。左、上、左上の各値をA,B,Cとすると、A,B,Cの中でA+B-Cの値に最も近いものがPaeth値になります。例えばA=10,B=15,C=20とすると、A+B-C=10+15-20=5なので、最も近いA=10が3つの値のPaeth値となります。
None
何もしません。各バイトの値をそのまま使用します。分かりやし~
各行に対してこれらのフィルタのうちどれかを適用します。各フィルタで引き算の結果がマイナスになった場合は255からループします。例えば-1→255, -2→254, -3→253となります。ファミコンのゲームのバグとかでありがちなやつですね。
deflate
フィルタ処理を行ったデータに対して、deflateという処理で圧縮を行います。このdeflateという処理はzipでも使われており、そこそこの圧縮率で、それなりの速さで完全に元に戻すことが出来ます。これがpngの中身でも使われており、概要でzipで圧縮していると書いたのはこのためです。zipをzipで圧縮してもほとんど小さくならない、という話を聞いたことがある人もいるかと思います。実際にjpegとpngの画像をそれぞれzip圧縮した所、サイズは以下のように変化しました。
フォーマット | 元画像のサイズ | zipファイルのサイズ | サイズの差分 | 圧縮率 |
---|---|---|---|---|
jpeg | 57,653 byte | 56,217 byte | 1,436 byte | 2.5% |
png | 451,516 byte | 450,985 byte | 531 byte | 0.12% |
やはりpngはzipとの相性が悪いことが分かりますね。ちなみにdeflateは後述するように、同じパターンがデータ中に何度も出てくるときに威力を発揮するので、ランレングス符号で連続する同じ符号をまとめているjpegもあまり相性は良くないかもしれません。
deflateは大きく分けて2つの処理に分けられます。LZSSとハフマン符号化です。
LZSS
LZSSは、いわゆる辞書式圧縮法と呼ばれるデータ圧縮法の一つです。具体的には、データ列の中のあるパターンと同じものが過去に表れていた場合、そのパターンを(何バイト前から、何バイト分)のパターンなのかという2つの値のペアに変換します。
この圧縮法では、データ列の中に同じパターンが何度も出現すればするほど効率よく圧縮することが出来ます。先ほどフィルタをかけたのはこのためで、出来るだけLZSSによる圧縮効率が良くなるようにデータを変換します。
どのフィルタをかけるべきかは、以下の方法で判別します。
まずは、あるフィルタAをかけた結果の値の中で128以上の数値をもつものから256を引きます。1バイトの値の範囲は1~255なので、その内128~255の値から256を引くと、値の範囲は1~127と-128~-1に変換できます。そうして変換したデータ列の各値の絶対値の総和を計算します。この値がフィルタAのスコアとなります。全てのフィルタに対してスコアを計算し、スコアが最小となるフィルタを使用します。何でこの方法で最適なフィルタが見つかるかって?知らねぇよ...
実際に、この方法は最適なフィルタを発見できるとは限りません。これはヒューリスティックな最適化アルゴリズムで、最適とは限らないけど、こうすればそれなりに良いフィルタを見つけることが出来る、というやり方です。また、なぜこのやり方をすればいいのかというのも経験則的なものらしいので、理論的な裏付けは無いらしいです。何で思いついたん?
ハフマン符号化
jpegの所で説明したよね?同じことは二度も言いません...!よく出てくる記号には短いビット列を割り当てるっていうやつです。
アニメーション
pngはgifのようにアニメーションさせることが出来ます。
出典 : https://commons.wikimedia.org/wiki/File:Alarm_Clock_Animation_High_Res.pngこれはapngというpngを拡張したフォーマットで、厳密にはpngとは異なるフォーマットなのですが、pngと互換性があり、apngに対応していないブラウザ等では普通に一枚絵のpng画像として表示されるようになっています。もしかして上の時計が動いていないですか?物を大切にしはるんどすなぁ...
apngの仕組みは非常にシンプルで、通常のpng画像の中に追加で複数のpng画像が埋め込まれています。png画像はチャンクというデータ単位で構成されています。このチャンクには必須のチャンクとオプションで付けるチャンクがあります。チャンクの中にはメタデータが書かれているIHDRやPLTEといったチャンクに加えて、画像データの本体が書かれているIDATというチャンクがあります。apngのフレームの一枚目はIDATチャンクの中に書かれています。2フレーム目以降はそれぞれがfdATというチャンクに書かれており、フレームの数だけfdATチャンクがIDATチャンクに加えて追加されます。fdATはオプションのチャンクなので、apngに対応していないソフトでは無視されて、1フレーム目のIDATチャンクの中身だけが普通のpngとして表示されます。[8]
pngのまとめ
さて、pngの中身について見てきましたが、正直jpegに比べればpngの中身を知る必要はそこまで無いと言えると思います。jpegは圧縮によって画像の見た目に影響が出るため、どのように圧縮しているのかを把握しておくと適切なフォーマット選びに役立つと思いますが、pngは可逆圧縮なので、画像の見た目には影響がありません。もちろん、画像の内容によって圧縮の度合いが違うので、そういう意味では圧縮の仕組みを知っておくのもいいかもしれませんが...また、これはjpegにも言える事なのですが、画像ファイルには画像そのものだけでなく、たくさんのメタデータも記録されています。とくに有名なのはexifでしょう。そういったメタデータについて詳しくなればパソコンの大先生に近づけるかもしれません。
おまけ:gif
gif画像についても少し触れたいと思います。ジフ?ギフ?gifです。Graphics Interchange Formatの略です。
皆さん大好きgifアニメでお馴染みのgifです。
出典 : https://commons.wikimedia.org/wiki/File:Rotating_earth_(large).gif
カラーパレット
gif画像の最大の特徴は256色しか扱えない事でしょう。通常、画像というのは、1ピクセルにつきRGBの3つの色の強さを1バイトずつ持つので、1ピクセルを3バイトで表現します。しかしgif画像では、事前に256色を決めておき、それらに番号を振り、各ピクセルでは何番の色を使うかという番号のみを持つことで1ピクセルを1バイトで表現できるようにしています。このように事前に色を決めておきその組み合わせで画像を表現する手法はカラーパレットと呼ばれ、ファミコンでも似たような方法でドット絵が描かれていました[9]。 このカラーパレットはアニメーション全体で1つのカラーパレットを共有する事も、1フレームごとに個別のカラーパレットを使うことも出来ます[10]し、1フレームを細かい領域に分割して各領域ごとにパレットを定義する事で、256色以上の色を完全に表現する事も出来ます[11]。ただし、当然カラーパレットをたくさん用意すればそれだけ容量が大きくなります。
jpeg画像とgif画像を見比べてみましょう。
1枚目がgifフォーマットの画像、2枚目がjpegフォーマットの画像です。ぱっと見では分かりませんが、メキシコ湾の辺りの水色の部分がgifでは消えていたり、カナダの山間部の深緑の部分に少しノイズのようなものが出来てしまっています。これはカラーパレットで表現しきれない色を似たような色で塗った結果、このようになってしまったものと思われます。
LZW
gif画像ではLZWという圧縮方式が使われており、このアルゴリズムの特許回りで一時期揉めていたらしいです[12]。LZWはLZSSと似た辞書式の圧縮アルゴリズムで、以下のような流れて圧縮を行います。
- 256個の記号を全て辞書に登録する。
- 現在の入力記号列の先頭から見て最も長く一致する記号列を辞書から探す。
- 入力記号列の先頭から、辞書内にあった記号列の分を削除し、代わりに辞書内の番号(インデックス)に置き換える。
- 今削除した記号列+次の1文字を辞書に追加する
- 2番に戻る
ここで、ポイントとなるのは4番の辞書に新たな記号列を追加する所で、これがある事によって、同じ記号が連続して並ぶような場面でどんどん大量の記号をまとめてインデックスに置き換えられるようになります。例えば0が10個連続して並んでいるような場合、この記号列にLZWを適用すると、(0が1個の記号列のインデックス)(0が2個の記号列のインデックス)(0が3個の記号列のインデックス)(0が4個の記号列のインデックス)という4つの数値にまとめることができてしまいます。gifはカラーパレットを使用する事で色数が少なくなっているので必然的に全く同じ色が並ぶ確率も上がっているので、LZWと相性がいいと言えるでしょう。
dithering
みなさん、何かガビガビのgif画像を見たことがありませんか?こう、砂嵐というかノイズというか...こういうやつです
ぱっと見気づきにくいかもしれません、特に北極の方などがかなりザラザラした見た目になっています。
このノイズみたいなやつは決して劣化しているわけではありません。そもそもgifは(カラーパレットによる減色を除けば)基本的に可逆圧縮ですし。(非可逆圧縮も使おうと思えば使えるみたいです[12])
これはditheringというテクニックで、色数の少ないカラーパレットを使用して、出来るだけ元の画像に近い色を表現するために、隣り合うピクセル同士の色が混ざった結果が元の色に近くなるようにしようという手法です。例えば、あるピクセルで紫色を表現するために、そのピクセルを赤色に、隣のピクセルを青色にする事で遠目に見た時に紫に見えるようにしよう、といった感じです。jpegの所で紹介した漫画の網点と同じく、人間の眼の、高い周波数成分を認識しづらいという特性を利用したものと言えるでしょう。ちなみに、同じようなことは画像だけでなく、オーディオでも行われています[13]。
ditheringはどちらかというと苦肉の策なので、出来るだけ使いたくありません。そのためには、出来るだけ元の色に近くなるような256色を選ぶ必要があります。元の画像からカラーパレットの256色を決定する処理を量子化と言います。量子化には色々なアルゴリズムがあり、それによって最終的なgif画像の見た目が変わってきます。
これらは順にPILを使用してmedian cut, maximum coverage, fast octreeという手法で量子化を行ったgif画像です[14]。メキシコ湾や北極に注目すると各量子化アルゴリズムの違いが分かるかと思います。どのアルゴリズムも一長一短なのが分かるかと思いますので、gif画像を作る際には量子化アルゴリズムを色々と試してみると良いでしょう。
おまけ2 : ベクタ画像(svg)
jpeg, pngそしておまけのgifと3種類の画像フォーマットを紹介してきましたが、これらの画像フォーマットは、画像をピクセルという単位で分割し、このピクセルの色を何らかの方法で表現している、という共通点を持っています。このような画像をラスタ画像と呼びます。
ラスタ画像には拡大出来ないという欠点があります。
こちらの画像は、ちん...フロリダ州の辺りを拡大したものです。1ピクセルがディスプレイの1ドットと対応しているうちはいいのですが、それ以上に画像を拡大しようとすると、どうしてもピクセル間の色を補完する必要が出てきます。この補完をどのように行うのかは表示するソフトによって異なるのですが、いずれにせよ見た目がある程度損なわれることになります。
一方svg画像(Scalable Vector Graphics)にはこの点を解消する、「無限に細部を拡大出来る」という強力な特徴があります。(こちらの画像はpngです!Qiitaのアップローダはsvgに対応していないので。直リンもちょっとあれだし...)
この画像を拡大します。
細部が依然としてはっきりしています。なぜこんな事が出来るのでしょうか?魔法か?だとしたら怖すぎ...
svg画像をテキストエディタで開いてみます。
なんかめっちゃ文字じゃん!そう、svg画像というのは画像と言いつつ、その実態はテキストファイル、もっと言えばXMLなのです。svg画像はこのようにテキスト形式で画像の数学的な図形としての情報を保持します。無限に拡大できるのも当然で、この情報を元にリアルタイムにどのような図形なのかを計算しているからです。このような画像フォーマットをベクタ画像と言います。
ラスタ画像で円を描くと、縁を拡大していけばどこかの時点でギザギザが見えてきてしまいますが、ベクタ画像で円を描いた場合は、無限に拡大しても動的に半径と中心座標等の情報を元にリアルタイムで円が描かれるため、どこまで行っても滑らかです。円のイデアです。
もちろんベクタ画像にも欠点があります。それは、複雑な図形に対応できない点です。例えば写真。実写のカラー画像には極めて複雑で有機的な図形がグラデーションを伴う色で存在しています。このような画像を図形に落とし込むことは不可能です。
例えばこちらは、あるサイトでjpeg画像をsvg画像に変換した結果(をスクショしたpng画像)です。
これを見た時、テレビ番組でその場にいない芸能人の顔がイラストで出てくるやつを思い出しました。あれ、実写の場合とイラストの場合でどういう違いがあるんですかね。
写真をsvgで表現しようとすると、このように色数を落としたり、いくつかのより単純な図形の組み合わせで似たように表現するしかないんでしょうね。一方で、昨今流行りのフラットデザインとはとても相性が良く、ボタンやアイコン等にはSVG画像がよく使用されています。後はフォントなどもベクタ形式が使われることが多いです。ベクタ形式のフォントをベクタフォントといい、ttf(True Type Font)等が有名ですね[15]。
まとめ
みなさんがネットでしょうもない画像をやりとりしてる裏では頭のいい先人達が考えたたくさんの仕組みが使われています。恥ずかしくないんか?
ソースコード
全てpythonです
jpegの各クオリティでの画像の保存
from PIL import Image
img = Image.open('./selfy.jpg')
img.save('./selfy_100.jpg', quality=100)
img.save('./selfy_50.jpg', quality=50)
img.save('./selfy_10.jpg', quality=10)
img.save('./selfy_1.jpg', quality=1)
YCbCrの各成分の擬似的なイメージの作成
from PIL import Image
y = Image.open('./selfy.jpg').convert('YCbCr')
Cb = Image.open('./selfy.jpg').convert('YCbCr')
Cr = Image.open('./selfy.jpg').convert('YCbCr')
def show(img, idx):
size = img.size
for x in range(size[0]):
for y in range(size[1]):
elems = list(img.getpixel((x, y)))
for i in range(3):
if i == idx:
continue
elems[i] = 128
img.putpixel((x, y), tuple(elems))
img.save('./{0}.jpg'.format(idx))
img.resize(((int)(size[0]/2), (int)(size[1]/2))
).save('./half_{0}.jpg'.format(idx))
show(y, 0)
show(Cb, 1)
show(Cr, 2)
jpeg画像の連続保存による劣化の検証
from PIL import Image
import time
orig_img = Image.open('./selfy.jpg')
orig_img.save('./tmp.jpg')
for i in range(0, 1001):
tmp_img = Image.open('./tmp.jpg')
if i == 1000:
tmp_img.save('./1000.jpg')
elif i == 500:
tmp_img.save('./500.jpg')
elif i == 100:
tmp_img.save('./100.jpg')
elif i == 1:
tmp_img.save('./1.jpg')
tmp_img.save('./tmp.jpg', quality=20)
time.sleep(0.2)
各量子化手法によるjpegからgifへの変換
from PIL import Image
img = Image.open('./selfy.jpg').convert("P")
img.save('selfy.gif', format="GIF", save_all=True)
median_cut = Image.open('./selfy.jpg').quantize(colors=256, method=0, dither=1)
median_cut.save('selfy_median_cut.gif', format="GIF", save_all=True)
maximum_coverage = Image.open('./selfy.jpg').quantize(colors=256, method=1, dither=1)
maximum_coverage.save("selfy_maximum_coverage.gif",format="GIF", save_all=True)
fast_octree = Image.open('./selfy.jpg').quantize(colors=256, method=2, dither=1)
fast_octree.save("selfy_fast_octree.gif", format="GIF", save_all=True)
参考文献
[1] テレビジョン学会誌, カラー画像工学の基礎と応用 (第 1 回) 視覚と色, 矢口博久
[3] Wikipedia, 網点
[5] 孤独なプログラマー, 新規保存で劣化していくJPEG形式の画像の様子をとらえ・・・てない気がする
[6] Wikipedia, Portable Network Graphics
[7] YouTube, How PNG Works: Compromising Speed for Quality, Reduicible, 1646秒付近
[8] Wikipedia, APNG
[10] YouTube, How GIF Works | GIF Compression Explained In 3 Minutes, CSRocks, 124秒付近
[11] Wikipedia, GIF
[12] Wikipedia, Graphics Interchange Format
[13] Wikipedia, Dither
[14] 某エンジニアのお仕事以外のメモ(分冊), Pythonで画像の減色をする
[15] Wikipedia, True Type