はじめに
一人でチマチマやってると、どこまでも深掘りしたくなりますね。次のステップへも行きたいので、ここらでまとめておきます。
必要なpython 環境は、最低限でよいです。
今回のワタシの環境は以下記事のようなものです。
macに入っているソフト、pythonのPackageリストをメモっとく - 放心ラボラトリ
どういったものをつくるか
ざっくりとは以下のような感じ。
- 指定したpngファイルを開く
- ヘッダの諸情報をゲット
- イメージ情報をゲット
- イメージ情報を渡して新たなpngファイルを出力
- 上記をclassとして実装する
- OpenCVやPILは使わない
今回作成したソースは、本記事の後ろの方に載せています。
特記事項的なものを先に書いておきます。
pngファイルのフォーマット
次のような構造になっています。
名前 |
長さ |
備考 |
signature |
8byte |
必ず最初に現れる |
IHDR |
25byte |
必ずsignatureの次に現れる |
PLE |
--byte |
なくても良い |
IDAT |
**byte |
複数OK |
... |
... |
... |
IEND |
4byte |
必ず最後に現れる |
大事な事をメモします。
- 最初はかならずsignature:8byte
- signatureの次は必ずIHDRチャンク:25byte
ここに色んな情報が入ってる
- イメージデータはIDATチャンクに入ってる
IDATは複数持てる。すなわち分割できるという事
- ファイルの終わりは必ずIENDチャンク:4byte
- 任意でその他様々なチャンクを持てる
本記事ではそれらは無視します
必要に応じてWEB検索
IHDRの構造
以下のようになっています。大事な情報なので全部読み出しておきます。
内容 |
長さ |
備考 |
データ長 |
4byte |
常に13 |
チャンクタイプ |
4byte |
常に'IHDR'が格納されている |
画像幅 |
4byte |
|
画像高さ |
4byte |
|
bit深度 |
1byte |
|
カラータイプ |
1byte |
|
圧縮メソッド |
1byte |
|
フィルターメソッド |
1byte |
|
インタレースメソッド |
1byte |
|
crc |
4byte |
チャンクタイプとデータから計算 |
各チャンクの構造
以下のようになっています。先に書いたIHDRも同じです。
名前 |
長さ |
備考 |
データ長 |
2byte |
データ長を格納 |
チャンク名 |
4byte |
『IHDR』とか『IDAT』など |
データ |
--byte |
チャンクのデータ |
crc |
4byte |
謝り検知用 |
チャンク名が出てくるまで順番に読んでいって、チャンク名がヒットしたら戻ってデータ長を確認、そのデータ長分データを読む、でいけそうです。crcはファイルに書き込むときは計算しないといけませんが、本日は標準ライブラリを使います。
import zlib
crc32 = zlib.crc32(data)
これでdataに対するcrcを得ることができます。
個別のメモ
1.指定したpngファイルを開く
open('filename','rb')
で開いてread()
していきます。signatureの2〜4byte目に必ず『png』があるので、これを目印にしてpngか否か判断します。
2.ファイルヘッダの諸情報をゲット
struct.unpack_from(">33s", bytedata, offset)
を使います。バイトデータの任意の場所から任意の長さを、任意の型で取り出すことができます。
逆にバイトデータにして書き込むときは
struct.pack(">I",data)
を使います。
バイトリテラルについて詳細やりだすとそれだけで長くなりそうなので別の機会にしますが、今回のような低レベル処理をしたい時には滅茶苦茶便利だと思います。
3.イメージ情報をゲット
'IDAT'を探しながらstruct.unpack_from()
で読み出していきます。読み出すたびにどんどん連結させていきます。最終的にはひとつの大きなIDATが出来ます。今後のため、一つになったIDATをgetするメソッドも実装しておきます。
4. イメージ情報を渡して新たなpngファイルを出力
これは、読み出したものをそのまま書き込めばよいです。ただし必須チャンクであるIHDR,IDAT,IENDしか書き込みません。そしてIDATは一つしか持たせません。元ファイルによりますが、多少は容量軽くなります。
まずは結果
以下のようなpng.pyというファイルを作成します。
import struct
import zlib
class png_img:
def __init__(self,inputfile):
self.f = open(inputfile,'rb')
self.imgdata = self.f.read()
#書き込み用にsignatureとIHDRを読み出しておく
self.head = struct.unpack_from(">33s", self.imgdata, 0)
#PNG画像か否か判断。PNG画像であれば各種データ読み出し。
if struct.unpack_from(">3s", self.imgdata, 1) == (b'PNG',):
self.i_width = struct.unpack_from(">I", self.imgdata, 16)
self.i_height = struct.unpack_from(">I", self.imgdata, 20)
self.bit_depth = struct.unpack_from(">B", self.imgdata, 24)
self.color_type = struct.unpack_from(">B", self.imgdata, 25)
self.comp_method = struct.unpack_from(">B", self.imgdata, 26)
self.filter_method = struct.unpack_from(">B", self.imgdata, 27)
self.interlace_method = struct.unpack_from(">B", self.imgdata, 28)
self.crc = struct.unpack_from(">B", self.imgdata, 29)
#IDATの読み出し。複数ある場合全て連結する。はIENDが現れるまで繰り返す
self.count = 30
self.idata_type = struct.unpack_from(">4s", self.imgdata, self.count)
self.img_length = 0 #IDATの合計データ長
self.img_data = b'' #IDATのデータ部が入る
self.cnt = 0 #IDATチャンクの数を数える
while self.idata_type != (b'IEND',):
self.idata_type = struct.unpack_from(">4s", self.imgdata, self.count)
if self.idata_type == (b'IDAT',):
self.idata_length = struct.unpack_from(">I",self.imgdata,self.count-4)
self.img_length += self.idata_length[0]
self.img_subdata = struct.unpack_from(">"+str(self.idata_length[0])+"s",self.imgdata,self.count+4)
self.img_data += self.img_subdata[0]
self.cnt += 1
self.count += 1
print('read OK','This Image is',self.count,'byte')
else:
print('This file is not PNG image')
self.f.close()
def outputPNG(self,outputfile):
self.ff=open(outputfile,'wb')
self.ff.write(struct.pack(">33s",self.head[0]))
self.ff.write(struct.pack(">I",self.img_length))
self.ff.write(struct.pack(">4s",b'IDAT'))
self.ff.write(struct.pack(">"+str(self.img_length)+"s",self.img_data))
self.ff.write(struct.pack(">I",zlib.crc32(b'IDAT' + self.img_data)))
self.ff.write(struct.pack(">I",0))
self.ff.write(struct.pack(">4s",b'IEND'))
self.ff.write(struct.pack(">I",zlib.crc32(b'IEND')))
self.ff.close()
def printDATA(self):
print('bit_depth =',self.bit_depth[0],'color_type =',self.color_type[0],
'comp_method =',self.comp_method[0],'filter_method =',self.filter_method[0],
'interlace_method =',self.interlace_method[0],'crc =',self.crc[0])
print('width =',self.i_width[0],', height =',self.i_height[0])
print('image data length =',self.img_length,' byte','IDAT THUNK cnt =',self.cnt)
def getIDAT(self):
return self.img_data
def searchTNK(self,thunk):
self.dmy1 = 0
self.dmy2 = 0
self.thunk_type = (b'',)
while self.thunk_type != (b'IEND',):
self.thunk_type = struct.unpack_from(">4s", self.imgdata, self.dmy1)
if self.thunk_type == (thunk,):
self.thunk_length = struct.unpack_from(">I",self.imgdata,self.dmy1-4)
self.thunk_value = struct.unpack_from(">B",self.imgdata,self.dmy1+4)
print(self.thunk_type[0],'length=',self.thunk_length[0],'value=',self.thunk_value[0])
self.dmy2 += 1
self.dmy1 += 1
if self.dmy2 == 0:
print('no thunk',thunk)
指定したチャンクがあるか探して、あれば表示もしてくれるsearchTNKメソッドも実装してみました。
同階層に以下内容のファイル作成します。元ファイル『lenna.png』も準備しておきます。
import png
inputfile='/Users/home/ip/lenna.png'
outputfile='/Users/home/ip/out.png'
p=png.png_img(inputfile)
p.printDATA()
p.outputPNG(outputfile)
p.searchTNK(b'sRGB')
p.searchTNK(b'PLTE')
実行結果は、terminalに以下のような表示がなされます。同階層にout.pngも生成されます。
read OK This Image is 521897 byte
bit_depth = 8 color_type = 6 comp_method = 0 filter_method = 0 interlace_method = 0 crc = 244
width = 512 , height = 512
image data length = 521078 byte IDAT THUNK cnt = 64
b'sRGB' length= 1 value= 0
no thunk b'PLTE'
なんとワタシのlennaさん、IDATチャンクが64もありました。
結び
pngファイルを読み込んで書き込むだけのものを作りました。やっていることはただの劣化コピーですね。OpenCVとかPILなどを使えば、劣化することなく数行で終えることができるでしょう。
しかし、今回ここに辿り着くまでに、
など、得られた知識は多くありました。
今後
pngファイル入出力関係は一旦一区切りです。ただ、crcのこととかbyteリテラルのこととかはまとめておきたいと思います。
さて、せっかくここまで作ったので、この流れで画像処理の基礎もやって見ようと思います。昨今流行りのDeepLearningの基礎の基礎の基礎くらいの内容です。
その前に得られたIDATの圧縮を解かねばなりませんね。ゆっくりやっていこうかと。
参考にさせていただいたサイト
こういったことをやりたい人は一定数いるのかなあ、という印象です。ワタシの記事よりわかりやすく正確なものがいっぱい。
PNGイメージのデータ構造を知ってみる(1) | エンジニアもどきの技術メモ
PNG イメージを自力でパースしてみる ~5/6 PNGフォーマット編~ - 自由研究ノート(仮)
pythonでバイナリデータを読む(PNGを例として) - Qiita
本サイト内関連記事
ダラダラのんびりやってきた記録。
pythonでPNGファイルを開いてみる - 放心ラボラトリ
pythonでpngファイルを開いてみる2 - 放心ラボラトリ
pythonでpngファイルを開いてみる3 - 放心ラボラトリ
pythonでpngファイルを開いてみる3.5 - 放心ラボラトリ