pythonでpngファイル入出力

はじめに

一人でチマチマやってると、どこまでも深掘りしたくなりますね。次のステップへも行きたいので、ここらでまとめておきます。

必要なpython 環境は、最低限でよいです。
今回のワタシの環境は以下記事のようなものです。

macに入っているソフト、pythonのPackageリストをメモっとく - 放心ラボラトリ

どういったものをつくるか

ざっくりとは以下のような感じ。

  1. 指定したpngファイルを開く
  2. ヘッダの諸情報をゲット
  3. イメージ情報をゲット
  4. イメージ情報を渡して新たなpngファイルを出力
  5. 上記をclassとして実装する
  6. 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フォーマットの構成
  • pythonでのバイナリデータの扱い方

など、得られた知識は多くありました。

今後

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 - 放心ラボラトリ