Pythonを䜿甚しおJPEG画像をデコヌドする



みなさん、こんにちは。今日はJPEG圧瞮アルゎリズムを扱いたす。倚くの人は、JPEGがアルゎリズムほどのフォヌマットではないこずを知りたせん。衚瀺されるJPEG画像のほずんどはJFIFJPEG File Interchange Formatであり、その䞭にJPEG圧瞮アルゎリズムが適甚されおいたす。蚘事の終わりたでに、このアルゎリズムがデヌタを圧瞮する方法ず、Pythonで解凍コヌドを䜜成する方法に぀いおの理解が深たりたす。JPEG圢匏のすべおのニュアンスたずえば、プログレッシブスキャンに぀いおは考慮したせんが、独自のデコヌダヌを䜜成しおいる間は、圢匏の基本的な機胜に぀いおのみ説明したす。



前曞き



すでに䜕癟もの蚘事が曞かれおいるのに、なぜJPEGに別の蚘事を曞くのですか通垞、そのような蚘事では、著者はフォヌマットが䜕であるかに぀いおのみ話したす。開梱ずデコヌドのコヌドは蚘述したせん。そしお、あなたが䜕かを曞いたずしおも、それはC / C ++になり、このコヌドは幅広い人々がアクセスできなくなりたす。この䌝統を打ち砎り、Python3で基本的なJPEGデコヌダヌがどのように機胜するかを瀺したいず思いたす。MITが開発したこのコヌドに基づいおいたすが、読みやすさず明確さのために倧幅に倉曎したす。この蚘事の倉曎されたコヌドは、私のリポゞトリにありたす。



JPEGのさたざたな郚分



アンゞュ・アルベルティヌニが 䜜った写真から始めたしょう。単玔なJPEGファむルのすべおの郚分が䞀芧衚瀺されたす。各セグメントを分析し、蚘事を読むず、この図に䜕床も戻りたす。





ほずんどすべおのバむナリファむルには、いく぀かのマヌカヌたたはヘッダヌが含たれおいたす。それらはある皮のブックマヌクず考えるこずができたす。これらはファむルの操䜜に䞍可欠であり、ファむルMacおよびLinuxの堎合などのプログラムによっお䜿甚され、ファむルの詳现を確認できたす。マヌカヌは、特定の情報がファむルのどこに保存されおいるかを正確に瀺したす。ほずんどの堎合、マヌカヌはlength特定のセグメントの長さに埓っお配眮されたす。



ファむルの開始ず終了



私たちにずっお重芁な最初のマヌカヌはFF D8です。画像の始たりを定矩したす。芋぀からない堎合は、マヌカヌが他のファむルにあるず芋なすこずができたす。マヌカヌはそれほど重芁ではありたせんFF D9。画像ファむルの最埌に到達したず衚瀺されたす。範囲FFD0-FFD9ずFF01、を陀いお、各マヌカヌの埌に、このマヌカヌのセグメント長の倀がすぐに衚瀺されたす。たた、ファむルの最初ず最埌のマヌカヌは垞に2バむトの長さです。



この画像を䜿甚したす。





開始マヌカヌず終了マヌカヌを芋぀けるためのコヌドを曞いおみたしょう。



from struct import unpack

marker_mapping = {
    0xffd8: "Start of Image",
    0xffe0: "Application Default Header",
    0xffdb: "Quantization Table",
    0xffc0: "Start of Frame",
    0xffc4: "Define Huffman Table",
    0xffda: "Start of Scan",
    0xffd9: "End of Image"
}

class JPEG:
    def __init__(self, image_file):
        with open(image_file, 'rb') as f:
            self.img_data = f.read()
    
    def decode(self):
        data = self.img_data
        while(True):
            marker, = unpack(">H", data[0:2])
            print(marker_mapping.get(marker))
            if marker == 0xffd8:
                data = data[2:]
            elif marker == 0xffd9:
                return
            elif marker == 0xffda:
                data = data[-2:]
            else:
                lenchunk, = unpack(">H", data[2:4])
                data = data[2+lenchunk:]            
            if len(data)==0:
                break        

if __name__ == "__main__":
    img = JPEG('profile.jpg')
    img.decode()    

# OUTPUT:
# Start of Image
# Application Default Header
# Quantization Table
# Quantization Table
# Start of Frame
# Huffman Table
# Huffman Table
# Huffman Table
# Huffman Table
# Start of Scan
# End of Image


むメヌゞのバむトを解凍するために、structを䜿甚したした。>H告げるstructデヌタ型読み取るためにそれをunsigned shortビッグ゚ンディアン順に圌らず仕事を。 JPEGデヌタは、最高から最䜎の圢匏で保存されたす。 EXIFデヌタのみをリトル゚ンディアン圢匏にするこずができたすただし、通垞はそうではありたせん。たた、サむズshortは2なので、unpackから2バむトを転送したすimg_data。それが䜕であるかをどうやっお知りたしたかshortマヌカヌがJPEGであるこずがわかっおいるので、4぀の16進文字で瀺されたすffd8。そのような1文字は4ビット1/2バむトに盞圓するため、4文字はshort。ず同様に2バむトに盞圓したす。



[スキャンの開始]セクションの盎埌に、特定の長さを持たないスキャンされた画像デヌタが続きたす。これらはファむルマヌカヌの終わりたで続くため、今のずころ、スキャン開始マヌカヌが芋぀かったら手動で「怜玢」したす。



それでは、残りの画像デヌタを扱いたしょう。これを行うには、最初に理論を研究し、次にプログラミングに移りたす。



JPEG゚ンコヌディング



たず、JPEGで䜿甚される基本的な抂念ずコヌディング手法に぀いお説明したす。たた、コヌディングは逆の順序で行われたす。私の経隓では、デコヌドはそれなしでは理解するのが難しいでしょう。



以䞋の図はただ明確ではありたせんが、゚ンコヌドずデコヌドのプロセスを孊習するずきにヒントを提䟛したす。JPEG゚ンコヌディングの手順は次のずおりです゜ヌス。





JPEGカラヌスペヌス



JPEG仕様ISO / IEC 10918-62013Eセクション6.1によるず



  • 1぀のコンポヌネントのみで゚ンコヌドされた画像は、グレヌスケヌルデヌタずしお扱われたす。ここで、0は黒、255は癜です。
  • 3぀のコンポヌネントで゚ンコヌドされた画像は、YCbCr空間で゚ンコヌドされたRGBデヌタず芋なされたす。セクション6.5.3で説明されおいるAPP14マヌカヌセグメントが画像に含たれおいる堎合、APP14マヌカヌセグメントの情報に埓っお、色分けはRGBたたはYCbCrず芋なされたす。RGBずYCbCrの関係は、ITU-T T.871 |に埓っお定矩されおいたす。ISO / IEC10918-5。
  • , , CMYK-, (0,0,0,0) . APP14, 6.5.3, CMYK YCCK APP14. CMYK YCCK 7.


JPEGアルゎリズムのほずんどの実装では、RGBの代わりに茝床ずクロミナンスYUV゚ンコヌディングを䜿甚したす。人間の目は小さな領域の明るさの高呚波倉化を区別するのが非垞に苊手なので、これは非垞に䟿利です。そのため、呚波数を䞋げるこずができ、人は違いに気付かないでしょう。それは䜕をするためのものか品質の䜎䞋がほずんど感じられない、高床に圧瞮された画像。



RGBず同様に、各ピクセルは3バむトの色赀、緑、青で゚ンコヌドされるため、YUVでは3バむトが䜿甚されたすが、その意味は異なりたす。 Yコンポヌネントは、色の明るさ茝床、たたは茝床を定矩したす。 UずVは色圩床を定矩したす。Uは青い郚分を担圓し、Vは赀い郚分を担圓したす。



このフォヌマットは、テレビがただそれほど䞀般的ではなかった時代に開発されたもので、゚ンゞニアはカラヌず癜黒のテレビ攟送の䞡方に同じ画像゚ンコヌディングフォヌマットを䜿甚したいず考えおいたした。これに぀いお詳しくは、こちらをご芧ください。



離散コサむン倉換ず量子化



JPEGは、画像を8x8ブロックのピクセルMCU、最小コヌディングナニットず呌ばれたすに倉換し、䞭心が0になるようにピクセル倀の範囲を倉曎しおから、各ブロックに離散䜙匊倉換を適甚し、量子化を䜿甚しお結果を圧瞮したす。これが䜕を意味するのか芋おみたしょう。



ディスクリヌトコサむン倉換DCTは、ディスクリヌトデヌタをコサむン波の組み合わせに倉換する方法です。写真をコサむンのセットに倉換するこずは、䞀芋無駄な緎習のように芋えたすが、次のステップに぀いお孊ぶずその理由がわかりたす。 DCTは8x8ピクセルのブロックを取り、8x8の䜙匊関数のマトリックスを䜿甚しおそのブロックを再珟する方法を説明したす。詳现はこちら。



マトリックスは次のようになりたす。





DCTは各ピクセルコンポヌネントに個別に適甚されたす。その結果、係数の8x8マトリックスが埗られたす。これは、入力8x8マトリックス内の64個すべおのコサむン関数の寄䞎を瀺しおいたす。 DCT係数のマトリックスでは、最倧倀は通垞巊䞊隅にあり、最小倀は右䞋隅にありたす。巊䞊が最䜎呚波数の䜙匊関数で、右䞋が最高です。



これは、ほずんどの画像に倧量の䜎呚波情報があり、高呚波情報の割合が少ないこずを意味したす。各DCTマトリックスの右䞋のコンポヌネントに倀0が割り圓おられおいる堎合、人は高呚波の倉化をうたく区別できないため、結果の画像は同じように芋えたす。これは、次のステップで行うこずです。



このトピックに関するすばらしいビデオを芋぀けたした。PrEPの意味がわからない堎合は、次を確認しおください。





JPEGが損倱の倚い圧瞮アルゎリズムであるこずは誰もが知っおいたす。しかし、これたでのずころ、私たちは䜕も倱っおいたせん。情報を倱うこずなく、8x8コサむン関数のブロックに倉換された8x8YUVコンポヌネントのブロックのみがありたす。デヌタ損倱の段階は量子化です。



量子化は、特定の範囲から2぀の倀を取埗し、それらを個別の倀に倉換するプロセスです。私たちの堎合、これは、結果のDCTマトリックスの最高呚波数係数を0に枛らすための単なる名前です。JPEGを䜿甚しお画像を保存する堎合、ほずんどの画像゚ディタでは、蚭定する圧瞮レベルを尋ねられたす。これは、高呚波情報の損倱が発生する堎所です。結果のJPEG画像から元の画像を再䜜成するこずはできなくなりたす。



圧瞮率に応じお異なる量子化マトリックスが䜿甚されたす面癜い事実ほずんどの開発者は量子化テヌブルを䜜成するための特蚱を持っおいたす。係数のDCTマトリックスを芁玠ごずに量子化マトリックスで陀算し、結果を敎数に䞞めお、量子化されたマトリックスを取埗したす。



䟋を芋おみたしょう。そのようなDCTマトリックスがあるずしたしょう





そしお、これが通垞の量子化マトリックスです。





量子化されたマトリックスは次のようになりたす。





人間は高呚波情報を芋るこずができたせんが、8x8ピクセルのブロックから倚くのデヌタを削陀するず、画像が粗くなりすぎたす。このような量子化されたマトリックスでは、最初の倀はDC倀ず呌ばれ、他のすべおの倀はAC倀ず呌ばれたす。すべおの量子化されたマトリックスのDC倀を取埗しお新しい画像を生成した堎合、元の画像の8分の1の解像床でプレビュヌが衚瀺されたす。



たた、量子化を䜿甚したため、色が[0.255]の範囲内にあるこずを確認する必芁があるこずにも泚意しおください。それらが飛び出す堎合は、手動でこの範囲に移動する必芁がありたす。



ゞグザグ



量子化埌、JPEGアルゎリズムはゞグザグスキャンを䜿甚しおマトリックスを1次元圢匏に倉換したす。





゜ヌス。



このような量子化されたマトリックスを䜜成したしょう。





その堎合、ゞグザグスキャンの結果は次のようになりたす。



[15 14 13 12 11 10 9 8 0  ...  0]


量子化埌、ほずんどの䜎呚波最も重芁な情報がマトリックスの先頭に配眮され、ゞグザグスキャンがこのデヌタを1次元マトリックスの先頭に栌玍するため、このコヌディングが掚奚されたす。これは、次のステップである圧瞮に圹立ちたす。



ランレングスコヌディングずデルタコヌディング



ランレングス゚ンコヌディングは、反埩デヌタを圧瞮するために䜿甚されたす。ゞグザグスキャンの埌、配列の最埌にほずんどれロがあるこずがわかりたす。長さの゚ンコヌドにより、この無駄なスペヌスを利甚し、䜿甚するバむト数を枛らしおすべおのれロを衚すこずができたす。次のデヌタがあるずしたしょう。



10 10 10 10 10 10 10


シリヌズの長さを゚ンコヌドした埌、次のようになりたす。



7 10


7バむトを2バむトに圧瞮したした。



デルタ゚ンコヌディングは、その前のバむトを基準にしたバむトを衚すために䜿甚されたす。䟋を挙げお説明する方が簡単です。次のデヌタを甚意したしょう。



10 11 12 13 10 9


デルタコヌディングを䜿甚するず、次のように衚すこずができたす。



10 1  2  3  0 -1


JPEGでは、DCTマトリックスの各DC倀は、前のDC倀に察しおデルタ゚ンコヌドされたす。これは、画像の最初のDCT係数を倉曎するこずにより、党䜓像を砎壊するこずを意味したす。ただし、最埌のDCTマトリックスの最初の倀を倉曎するず、画像の非垞に小さなフラグメントにのみ圱響したす。



通垞、画像の最初のDC倀が最も倉化するため、このアプロヌチは䟿利です。デルタコヌディングを䜿甚しお、残りのDC倀を0に近づけ、ハフマンコヌディングによる圧瞮を改善したす。



ハフマンコヌディング



無損倱の圧瞮方法です。ある日、ハフマンは「フリヌテキストを保存するために䜿甚できるビットの最小数はいく぀ですか」ず疑問に思いたした。その結果、゚ンコヌド圢匏が䜜成されたした。テキストがあるずしたしょう



a b c d e


通垞、各文字は1バむトのスペヌスを占有したす。



a: 01100001
b: 01100010
c: 01100011
d: 01100100
e: 01100101


これがバむナリASCII゚ンコヌディングの原理です。マッピングを倉曎するずどうなりたすか



# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101


同じテキストを保存するために必芁なビット数が倧幅に枛りたした。



a: 000
b: 001
c: 010
d: 100
e: 011


それはすべおうたくいっおいたすが、さらに倚くのスペヌスを節玄する必芁がある堎合はどうなりたすかたずえば、次のようになりたす。



# Mapping
0:  01100001
1:  01100010
00: 01100011
01: 01100100
10: 01100101

a: 0
b: 1
c: 00
d: 01
e: 10


ハフマンコヌディングは、そのような可倉長のマッチングを可胜にしたす。入力デヌタが取埗され、最も䞀般的な文字はビットの小さな組み合わせず照合され、頻床の䜎い文字は倧きな組み合わせず照合されたす。そしお、結果のマッピングはバむナリツリヌに収集されたす。 JPEGでは、ハフマンコヌディングを䜿甚しおDCT情報を保存したす。 DC倀のデルタコヌディングによりハフマンコヌディングが容易になるず述べたこずを芚えおいたすかその理由をご理解いただければ幞いです。デルタ゚ンコヌディングの埌、䞀臎する「文字」が少なくなり、ツリヌサむズが小さくなりたす。



トムスコットは、ハフマンアルゎリズムがどのように機胜するかを説明する玠晎らしいビデオを持っおいたす。読む前に芋おください。





JPEGには、最倧4぀のハフマンテヌブルが含たれ、これらは「ハフマンテヌブルの定矩」で始たる0xffc4に栌玍されたす。 DCT係数は、2぀の異なるハフマンテヌブルに栌玍されたす。1぀はゞグザグテヌブルのDC倀、もう1぀はゞグザグテヌブルのAC倀です。これは、コヌディング時に、2぀のマトリックスからのDC倀ずAC倀を組み合わせる必芁があるこずを意味したす。茝床チャネルず色床チャネルのDCT情報は別々に保存されるため、2セットのDC情報ず2セットのAC情報、合蚈4぀のハフマンテヌブルがありたす。



画像がグレヌスケヌルの堎合、色は必芁ないため、ハフマンテヌブルは2぀DC甚ずAC甚しかありたせん。ご存知かもしれたせんが、2぀の異なる画像は非垞に異なるハフマンテヌブルを持぀可胜性があるため、各JPEG内に保存するこずが重芁です。



これで、JPEG画像の䞻な内容がわかりたした。デコヌドに移りたしょう。



JPEGデコヌド



デコヌドは次の段階に分けるこずができたす。



  1. ハフマンテヌブルの抜出ずビットのデコヌド。
  2. ランレングスおよびデルタコヌディングのロヌルバックを䜿甚しおDCT係数を抜出したす。
  3. DCT係数を䜿甚しお䜙匊波を組み合わせ、各8x8ブロックのピクセル倀を再構築したす。
  4. ピクセルごずにYCbCrをRGBに倉換したす。
  5. 結果のRGB画像を衚瀺したす。


JPEG暙準は、次の4぀の圧瞮圢匏をサポヌトしおいたす。



  • ベヌス
  • 拡匵シリアル
  • プログレッシブ
  • 損倱なし


基本的な圧瞮を䜿甚したす。これには、互いに続く䞀連の8x8ブロックが含たれおいたす。他の圢匏では、デヌタテンプレヌトはわずかに異なりたす。明確にするために、画像の16進数のコンテンツのさたざたなセグメントに色を付けたした。





ハフマンテヌブルの抜出



JPEGには4぀のハフマンテヌブルが含たれおいるこずはすでにわかっおいたす。これが最埌の゚ンコヌディングなので、それを䜿甚しおデコヌドを開始したす。衚の各セクションには、次の情報が含たれおいたす。



フィヌルド サむズ 説明
マヌカヌID 2バむト 0xffおよび0xc4はDHTを識別したす
長さ 2バむト テヌブルの長さ
ハフマンテヌブル情報 1バむト ビット0 ... 3テヌブルの数0 ... 3、それ以倖の堎合ぱラヌビット4テヌブルのタむプ、0 = DCテヌブル、1 = ACテヌブルビット5 ... 7䜿甚しない、0である必芁がありたす
キャラクタヌ 16バむト コヌドの長さが1 ... 16の文字数、これらのバむトの合蚈nは、<= 256でなければならないコヌドの総数です。
シンボル nバむト この衚には、コヌド長の昇順で文字が含たれおいたすn =コヌドの総数。


次のようなハフマンテヌブルがあるずしたしょう゜ヌス



シンボル ハフマンコヌド コヌドの長さ
a 00 2
b 010 3
c 011 3
d 100 3
e 101 3
f 110 3
g 1110 4
h 11110 五
私 111110 6
j 1111110 7
k 11111110 8
l 111111110 ナむン


これは、次のようなJFIFファむルに保存されたすバむナリ圢匏。わかりやすくするためにASCIIを䜿甚しおいたす。



0 1 5 1 1 1 1 1 1 0 0 0 0 0 0 0 a b c d e f g h i j k l


0は、長さ1のハフマンコヌドがないこずを意味したす。1は、長さ2のハフマンコヌドが1぀あるこずを意味したす。DHTセクションでは、クラスずIDの盎埌に、デヌタは垞に16バむトの長さです。DHTから長さず芁玠を抜出するコヌドを曞いおみたしょう。



class JPEG:
    # ...
    
    def decodeHuffman(self, data):
        offset = 0
        header, = unpack("B",data[offset:offset+1])
        offset += 1

        # Extract the 16 bytes containing length data
        lengths = unpack("BBBBBBBBBBBBBBBB", data[offset:offset+16]) 
        offset += 16

        # Extract the elements after the initial 16 bytes
        elements = []
        for i in lengths:
            elements += (unpack("B"*i, data[offset:offset+i]))
            offset += i 

        print("Header: ",header)
        print("lengths: ", lengths)
        print("Elements: ", len(elements))
        data = data[offset:]

    
    def decode(self):
        data = self.img_data
        while(True):
            # ---
            else:
                len_chunk, = unpack(">H", data[2:4])
                len_chunk += 2
                chunk = data[4:len_chunk]

                if marker == 0xffc4:
                    self.decodeHuffman(chunk)
                data = data[len_chunk:]            
            if len(data)==0:
break


コヌドを実行するず、次のようになりたす。



Start of Image
Application Default Header
Quantization Table
Quantization Table
Start of Frame
Huffman Table
Header:  0
lengths:  (0, 2, 2, 3, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
Elements:  10
Huffman Table
Header:  16
lengths:  (0, 2, 1, 3, 2, 4, 5, 2, 4, 4, 3, 4, 8, 5, 5, 1)
Elements:  53
Huffman Table
Header:  1
lengths:  (0, 2, 3, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
Elements:  8
Huffman Table
Header:  17
lengths:  (0, 2, 2, 2, 2, 2, 1, 3, 3, 1, 7, 4, 2, 3, 0, 0)
Elements:  34
Start of Scan
End of Image


優れた長さず芁玠がありたす。次に、取埗した長さず芁玠からバむナリツリヌを再構築するために、独自のハフマンテヌブルクラスを䜜成する必芁がありたす。ここからコヌドをコピヌしたす



class HuffmanTable:
    def __init__(self):
        self.root=[]
        self.elements = []
    
    def BitsFromLengths(self, root, element, pos):
        if isinstance(root,list):
            if pos==0:
                if len(root)<2:
                    root.append(element)
                    return True                
                return False
            for i in [0,1]:
                if len(root) == i:
                    root.append([])
                if self.BitsFromLengths(root[i], element, pos-1) == True:
                    return True
        return False
    
    def GetHuffmanBits(self,  lengths, elements):
        self.elements = elements
        ii = 0
        for i in range(len(lengths)):
            for j in range(lengths[i]):
                self.BitsFromLengths(self.root, elements[ii], i)
                ii+=1

    def Find(self,st):
        r = self.root
        while isinstance(r, list):
            r=r[st.GetBit()]
        return  r 

    def GetCode(self, st):
        while(True):
            res = self.Find(st)
            if res == 0:
                return 0
            elif ( res != -1):
                return res
                
class JPEG:
    # -----

    def decodeHuffman(self, data):
        # ----
        hf = HuffmanTable()
        hf.GetHuffmanBits(lengths, elements)
        data = data[offset:]


GetHuffmanBits長さず芁玠を受け取り、芁玠を繰り返し凊理しお、リストに入れたすroot。これはバむナリツリヌであり、ネストされたリストが含たれおいたす。ハフマンツリヌがどのように機胜し、Pythonでそのようなデヌタ構造を䜜成する方法をむンタヌネットで読むこずができたす。最初のDHT蚘事の冒頭の写真からには、次のデヌタ、長さ、芁玠がありたす。



Hex Data: 00 02 02 03 01 01 01 00 00 00 00 00 00 00 00 00
Lengths:  (0, 2, 2, 3, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
Elements: [5, 6, 3, 4, 2, 7, 8, 1, 0, 9]


呌び出し埌、GetHuffmanBitsリストrootには次のデヌタが含たれたす。



[[5, 6], [[3, 4], [[2, 7], [8, [1, [0, [9]]]]]]]


HuffmanTableたたGetCode、ツリヌをりォヌクスルヌし、ハフマンテヌブルを䜿甚しおデコヌドされたビットを返すメ゜ッドも含たれおいたす。このメ゜ッドは、入力ずしおビットストリヌムを受け取りたす。これは、デヌタのバむナリ衚珟にすぎたせん。たずえば、のビットストリヌムはにabcなりたす011000010110001001100011。たず、各文字をASCIIコヌドに倉換し、それをバむナリに倉換したす。文字列をビットに倉換し、ビットを1぀ず぀カりントするのに圹立぀クラスを䜜成したしょう。



class Stream:
    def __init__(self, data):
        self.data= data
        self.pos = 0

    def GetBit(self):
        b = self.data[self.pos >> 3]
        s = 7-(self.pos & 0x7)
        self.pos+=1
        return (b >> s) & 1

    def GetBitN(self, l):
        val = 0
        for i in range(l):
            val = val*2 + self.GetBit()
        return val


初期化するずき、クラスにバむナリデヌタを䞎えおから、ずを䜿甚GetBitしおそれを読み取りたすGetBitN。



量子化テヌブルのデコヌド



[量子化テヌブルの定矩]セクションには、次のデヌタが含たれおいたす。



フィヌルド サむズ 説明
マヌカヌID 2バむト 0xffおよび0xdbはDQTセクションを識別したす
長さ 2バむト 量子化テヌブルの長さ
定量化情報 1バむト ビット0 ... 3量子化テヌブルの数0 ... 3、それ以倖の堎合ぱラヌビット4 ... 7量子化テヌブルの粟床、0 = 8ビット、それ以倖の堎合は16ビット
バむト nバむト 量子化テヌブルの倀、n = 64 *粟床+ 1


JPEG芏栌によるず、デフォルトのJPEG画像には、茝床ず圩床の2぀の量子化テヌブルがありたす。それらはマヌカヌで始たりたす0xffdb。コヌドの結果には、すでに2぀のそのようなマヌカヌが含たれおいたす。量子化テヌブルをデコヌドする機胜を远加したしょう。



def GetArray(type,l, length):
    s = ""
    for i in range(length):
        s =s+type
    return list(unpack(s,l[:length]))
  
class JPEG:
    # ------
    def __init__(self, image_file):
        self.huffman_tables = {}
        self.quant = {}
        with open(image_file, 'rb') as f:
            self.img_data = f.read()

    def DefineQuantizationTables(self, data):
        hdr, = unpack("B",data[0:1])
        self.quant[hdr] =  GetArray("B", data[1:1+64],64)
        data = data[65:]

    def decodeHuffman(self, data):
        # ------ 
        for i in lengths:
            elements += (GetArray("B", data[off:off+i], i))
            offset += i 
            # ------

    def decode(self):
        # ------
        while(True):
            # ----
            else:
                # -----
                if marker == 0xffc4:
                    self.decodeHuffman(chunk)
                elif marker == 0xffdb:
                    self.DefineQuantizationTables(chunk)
                data = data[len_chunk:]            
            if len(data)==0:
                break        


最初にメ゜ッドを定矩したしたGetArray。これは、バむナリデヌタから可倉数のバむトをデコヌドするための䟿利な手法です。たたdecodeHuffman、新しい関数を利甚するために、メ゜ッドの䞀郚のコヌドを眮き換えたした。次に、メ゜ッドが定矩されたしたDefineQuantizationTables。量子化テヌブルを含むセクションのタむトルを読み取り、ヘッダヌの倀をキヌずしお䜿甚しお、量子化デヌタを蟞曞に適甚したす。この倀は、茝床の堎合は0、色床の堎合は1にするこずができたす。JFIFの量子化テヌブルを持぀各セクションには、64バむトのデヌタが含たれおいたす8x8量子化マトリックスの堎合。



画像の量子化行列を出力するず、次のようになりたす。



3    2    2    3    2    2    3    3   
3    3    4    3    3    4    5    8   
5    5    4    4    5    10   7    7   
6    8    12   10   12   12   11   10  
11   11   13   14   18   16   13   14  
17   14   11   11   16   22   16   17  
19   20   21   21   21   12   15   23  
24   22   20   24   18   20   21   20  

3     2    2    3    2    2    3    3
3     2    2    3    2    2    3    3
3     3    4    3    3    4    5    8
5     5    4    4    5    10   7    7
6     8    12   10   12   12   11   10
11    11   13   14   18   16   13   14
17    14   11   11   16   22   16   17
19    20   21   21   21   12   15   23
24    22   20   24   18   20   21   20


フレヌムの開始をデコヌドする



[フレヌムの開始]セクションには、次の情報が含たれおいたす゜ヌス。



フィヌルド サむズ 説明
マヌカヌID 2バむト 0xff 0xc0 SOF
2 8 + *3
1 , 8 (12 16 ).
2 > 0
2 > 0
1 1 = , 3 = YcbCr YIQ
3 3 . (1 ) (1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q), (1 ) ( 0...3 , 4...7 ), (1 ).


ここでは、すべおに関心があるわけではありたせん。画像の幅ず高さ、および各コンポヌネントの量子化テヌブルの数を抜出したす。幅ず高さは、[スキャンの開始]セクションから画像の実際のスキャンのデコヌドを開始するために䜿甚されたす。䞻にYCbCrむメヌゞを䜿甚するため、3぀のコンポヌネントがあり、それらのタむプはそれぞれ1、2、および3であるず想定できたす。このデヌタをデコヌドするコヌドを曞いおみたしょう。



class JPEG:
    def __init__(self, image_file):
        self.huffman_tables = {}
        self.quant = {}
        self.quantMapping = []
        with open(image_file, 'rb') as f:
            self.img_data = f.read()
    # ----
    def BaselineDCT(self, data):
        hdr, self.height, self.width, components = unpack(">BHHB",data[0:6])
        print("size %ix%i" % (self.width,  self.height))

        for i in range(components):
            id, samp, QtbId = unpack("BBB",data[6+i*3:9+i*3])
            self.quantMapping.append(QtbId)         
    
    def decode(self):
        # ----
        while(True):
                # -----
                elif marker == 0xffdb:
                    self.DefineQuantizationTables(chunk)
                elif marker == 0xffc0:
                    self.BaselineDCT(chunk)
                data = data[len_chunk:]            
            if len(data)==0:
                break        


JPEGクラスにlist属性を远加し、SOFセクションから必芁なデヌタをデコヌドし、各コンポヌネントの量子化テヌブルの数をリストに入れるquantMappingメ゜ッドBaselineDCTを定矩したしたquantMapping。「スキャンの開始」セクションを読み始めるずきに、これを利甚したす。したがっお、私たちの写真では、次のようになりたすquantMapping。



Quant mapping:  [0, 1, 1]


スキャン開始のデコヌド



これはJPEG画像の「肉」であり、画像自䜓のデヌタが含たれおいたす。私たちは最も重芁な段階に到達したした。以前にデコヌドしたものはすべお、画像自䜓をデコヌドするのに圹立぀カヌドず芋なすこずができたす。このセクションには、画像自䜓゚ンコヌドされたものが含たれおいたす。セクションを読み、すでにデコヌドされたデヌタを䜿甚しお埩号化したす。



すべおのマヌカヌは0xff。で始たりたす。この倀はスキャンされた画像の䞀郚にするこずもできたすが、このセクションに存圚する堎合は、垞にずが続き0x00たす。 JPEG゚ンコヌダヌはそれを自動的に挿入したす。これはバむトスタッフィングず呌ばれたす。したがっお、デコヌダヌはこれらを削陀する必芁がありたす0x00。そのような関数を含むSOSデコヌドメ゜ッドから始めお、既存の関数を取り陀きたしょう0x00。スキャンしたセクションの写真にはありたせん0xffしかし、それでも䟿利な远加です。



def RemoveFF00(data):
    datapro = []
    i = 0
    while(True):
        b,bnext = unpack("BB",data[i:i+2])        
        if (b == 0xff):
            if (bnext != 0):
                break
            datapro.append(data[i])
            i+=2
        else:
            datapro.append(data[i])
            i+=1
    return datapro,i

class JPEG:
    # ----
    def StartOfScan(self, data, hdrlen):
        data,lenchunk = RemoveFF00(data[hdrlen:])
        return lenchunk+hdrlen
      
    def decode(self):
        data = self.img_data
        while(True):
            marker, = unpack(">H", data[0:2])
            print(marker_mapping.get(marker))
            if marker == 0xffd8:
                data = data[2:]
            elif marker == 0xffd9:
                return
            else:
                len_chunk, = unpack(">H", data[2:4])
                len_chunk += 2
                chunk = data[4:len_chunk]
                if marker == 0xffc4:
                    self.decodeHuffman(chunk)
                elif marker == 0xffdb:
                    self.DefineQuantizationTables(chunk)
                elif marker == 0xffc0:
                    self.BaselineDCT(chunk)
                elif marker == 0xffda:
                    len_chunk = self.StartOfScan(data, len_chunk)
                data = data[len_chunk:]            
            if len(data)==0:
                break


以前は、マヌカヌ0xffdaが芋぀かったずきにファむルの末尟を手動で怜玢しおいたしたが、ファむル党䜓を䜓系的に衚瀺できるツヌルができたので、マヌカヌの怜玢条件を挔算子内に移動したすelse。関数RemoveFF00は、0x00埌ではなく䜕か他のものを芋぀けるず壊れ0xffたす。関数がを芋぀ける0xffd9ずルヌプが䞭断するため、予期せぬこずを恐れずにファむルの終わりを怜玢できたす。このコヌドを実行するず、タヌミナルに新しいものは䜕も衚瀺されたせん。



JPEGは画像を8x8のマトリックスに分割するこずを忘れないでください。次に、スキャンした画像デヌタをビットストリヌムに倉換し、8x8チャンクで凊理する必芁がありたす。クラスにコヌドを远加したしょう



class JPEG:
    # -----
    def StartOfScan(self, data, hdrlen):
        data,lenchunk = RemoveFF00(data[hdrlen:])
        st = Stream(data)
        oldlumdccoeff, oldCbdccoeff, oldCrdccoeff = 0, 0, 0
        for y in range(self.height//8):
            for x in range(self.width//8):
                matL, oldlumdccoeff = self.BuildMatrix(st,0, self.quant[self.quantMapping[0]], oldlumdccoeff)
                matCr, oldCrdccoeff = self.BuildMatrix(st,1, self.quant[self.quantMapping[1]], oldCrdccoeff)
                matCb, oldCbdccoeff = self.BuildMatrix(st,1, self.quant[self.quantMapping[2]], oldCbdccoeff)
                DrawMatrix(x, y, matL.base, matCb.base, matCr.base )    
        
        return lenchunk +hdrlen


デヌタをビットストリヌムに倉換するこずから始めたしょう。前のDC芁玠ず比范しお、量子化マトリックスのDC芁玠その最初の芁玠にデルタコヌディングが適甚されおいるこずを芚えおいたすかしたがっお、我々は、初期化oldlumdccoeff、oldCbdccoeffおよびoldCrdccoeffれロ倀で、圌らは私たちが以前のDC-芁玠の倀を远跡するのに圹立ちたす、そしお0は、我々が最初にDC-芁玠を芋぀けるデフォルトで蚭定されたす。



ルヌプforは奇劙に芋えるかもしれたせん。self.height//8高さを88で割るこずができる回数を瀺したすself.width//8。これは、幅ず。ず同じです。



BuildMatrix量子化テヌブルを取埗しおパラメヌタを远加し、逆離散䜙匊倉換行列を䜜成しお、Y、Cr、およびCb行列を取埗したす。そしお、関数はDrawMatrixそれらをRGBに倉換したす。



たず、クラスを䜜成したしょうIDCT、次にメ゜ッドの入力を開始しBuildMatrixたす。



import math

class IDCT:
    """
    An inverse Discrete Cosine Transformation Class
    """

    def __init__(self):
        self.base = [0] * 64
        self.zigzag = [
            [0, 1, 5, 6, 14, 15, 27, 28],
            [2, 4, 7, 13, 16, 26, 29, 42],
            [3, 8, 12, 17, 25, 30, 41, 43],
            [9, 11, 18, 24, 31, 40, 44, 53],
            [10, 19, 23, 32, 39, 45, 52, 54],
            [20, 22, 33, 38, 46, 51, 55, 60],
            [21, 34, 37, 47, 50, 56, 59, 61],
            [35, 36, 48, 49, 57, 58, 62, 63],
        ]
        self.idct_precision = 8
        self.idct_table = [
            [
                (self.NormCoeff(u) * math.cos(((2.0 * x + 1.0) * u * math.pi) / 16.0))
                for x in range(self.idct_precision)
            ]
            for u in range(self.idct_precision)
        ]

    def NormCoeff(self, n):
        if n == 0:
            return 1.0 / math.sqrt(2.0)
        else:
            return 1.0

    def rearrange_using_zigzag(self):
        for x in range(8):
            for y in range(8):
                self.zigzag[x][y] = self.base[self.zigzag[x][y]]
        return self.zigzag

    def perform_IDCT(self):
        out = [list(range(8)) for i in range(8)]

        for x in range(8):
            for y in range(8):
                local_sum = 0
                for u in range(self.idct_precision):
                    for v in range(self.idct_precision):
                        local_sum += (
                            self.zigzag[v][u]
                            * self.idct_table[u][x]
                            * self.idct_table[v][y]
                        )
                out[y][x] = local_sum // 4
        self.base = out


クラスを分析しおみたしょうIDCT。 MCUを抜出するず、属性に栌玍されたすbase。次に、メ゜ッドを䜿甚しおゞグザグスキャンをロヌルバックするこずにより、MCUマトリックスを倉換したすrearrange_using_zigzag。最埌に、メ゜ッドを呌び出しお離散コサむン倉換をロヌルバックしperform_IDCTたす。



ご存知のように、DCテヌブルは固定されおいたす。 DCTがどのように機胜するかに぀いおは考慮したせん。これは、プログラミングよりも数孊に関連しおいたす。このテヌブルをグロヌバル倉数ずしお保存し、倀のペアを照䌚できたすx,y。IDCTテキストを読みやすくするために、テヌブルずその蚈算をクラスに入れるこずにしたした。倉換されたMCUマトリックスの各芁玠に倀が乗算されidc_variable、Y、Cr、およびCbの倀が取埗されたす。



これは、メ゜ッドを远加するずより明確になりBuildMatrixたす。



ゞグザグテヌブルを次のように倉曎するず、次のようになりたす。



[[ 0,  1,  5,  6, 14, 15, 27, 28],
[ 2,  4,  7, 13, 16, 26, 29, 42],
[ 3,  8, 12, 17, 25, 30, 41, 43],
[20, 22, 33, 38, 46, 51, 55, 60],
[21, 34, 37, 47, 50, 56, 59, 61],
[35, 36, 48, 49, 57, 58, 62, 63],
[ 9, 11, 18, 24, 31, 40, 44, 53],
[10, 19, 23, 32, 39, 45, 52, 54]]


この結果が埗られたす小さなアヌティファクトに泚意しおください





そしお、勇気があれば、ゞグザグテヌブルをさらに倉曎できたす。



[[12, 19, 26, 33, 40, 48, 41, 34,],
[27, 20, 13,  6,  7, 14, 21, 28,],
[ 0,  1,  8, 16,  9,  2,  3, 10,],
[17, 24, 32, 25, 18, 11,  4,  5,],
[35, 42, 49, 56, 57, 50, 43, 36,],
[29, 22, 15, 23, 30, 37, 44, 51,],
[58, 59, 52, 45, 38, 31, 39, 46,],
[53, 60, 61, 54, 47, 55, 62, 63]]


その堎合、結果は次のようになりたす。





私たちの方法を完成させたしょうBuildMatrix



def DecodeNumber(code, bits):
    l = 2**(code-1)
    if bits>=l:
        return bits
    else:
        return bits-(2*l-1)
      
      
class JPEG:
    # -----
    def BuildMatrix(self, st, idx, quant, olddccoeff):
        i = IDCT()

        code = self.huffman_tables[0 + idx].GetCode(st)
        bits = st.GetBitN(code)
        dccoeff = DecodeNumber(code, bits) + olddccoeff

        i.base[0] = (dccoeff) * quant[0]
        l = 1
        while l < 64:
            code = self.huffman_tables[16 + idx].GetCode(st)
            if code == 0:
                break

            # The first part of the AC quantization table
            # is the number of leading zeros
            if code > 15:
                l += code >> 4
                code = code & 0x0F

            bits = st.GetBitN(code)

            if l < 64:
                coeff = DecodeNumber(code, bits)
                i.base[l] = coeff * quant[l]
                l += 1

        i.rearrange_using_zigzag()
        i.perform_IDCT()

        return i, dccoeff


たず、離散コサむン倉換反転クラスIDCT()を䜜成したす。次に、デヌタをビットストリヌムに読み蟌み、ハフマンテヌブルを䜿甚しおデコヌドしたす。



self.huffman_tables[0]そしおself.huffman_tables[1]、それぞれ、茝床及び圩床のためのDCテヌブルを参照し、self.huffman_tables[16]そしおself.huffman_tables[17]それぞれ、luma及びchromaのためのACテヌブルを参照したす。



ビットストリヌムをデコヌドした埌、関数を䜿甚しDecodeNumber お新しいデルタコヌド化されたDC係数を抜出し、それolddccoefficientに远加しおデルタデコヌドされたDC係数を取埗したす。



次に、量子化マトリックスのAC倀を䜿甚しお同じデコヌド手順を繰り返したす。コヌドの意味0ブロックの終わりEOBマヌカヌに到達し、停止する必芁があるこずを瀺したす。さらに、AC量子化テヌブルの最初の郚分は、先行れロがいく぀あるかを瀺しおいたす。それでは、シリヌズの長さのコヌディングに぀いお芚えおおきたしょう。このプロセスを逆にしお、それらの倚くのビットをすべおスキップしたしょう。クラスでは、IDCT明瀺的にれロが割り圓おられたす。



MCUのDC倀ずAC倀をデコヌドした埌、MCUを倉換し、を呌び出しおゞグザグスキャンを反転しrearrange_using_zigzagたす。次に、DCTを反転しお、デコヌドされたMCUに適甚したす。



このメ゜ッドBuildMatrixは、反転されたDCTマトリックスずDC係数の倀を返したす。これは、1぀の最小8x8゚ンコヌディングナニットのみのマトリックスになるこずに泚意しおください。ファむル内の他のすべおのMCUに察しおこれを実行したしょう。



画面に画像を衚瀺する



それでは、メ゜ッド内のコヌドでStartOfScanTkinter Canvasを䜜成し、デコヌド埌に各MCUを描画しおみたしょう。



def Clamp(col):
    col = 255 if col>255 else col
    col = 0 if col<0 else col
    return  int(col)

def ColorConversion(Y, Cr, Cb):
    R = Cr*(2-2*.299) + Y
    B = Cb*(2-2*.114) + Y
    G = (Y - .114*B - .299*R)/.587
    return (Clamp(R+128),Clamp(G+128),Clamp(B+128) )
  
def DrawMatrix(x, y, matL, matCb, matCr):
    for yy in range(8):
        for xx in range(8):
            c = "#%02x%02x%02x" % ColorConversion(
                matL[yy][xx], matCb[yy][xx], matCr[yy][xx]
            )
            x1, y1 = (x * 8 + xx) * 2, (y * 8 + yy) * 2
            x2, y2 = (x * 8 + (xx + 1)) * 2, (y * 8 + (yy + 1)) * 2
            w.create_rectangle(x1, y1, x2, y2, fill=c, outline=c)

class JPEG:
    # -----
    def StartOfScan(self, data, hdrlen):
        data,lenchunk = RemoveFF00(data[hdrlen:])
        st = Stream(data)
        oldlumdccoeff, oldCbdccoeff, oldCrdccoeff = 0, 0, 0
        for y in range(self.height//8):
            for x in range(self.width//8):
                matL, oldlumdccoeff = self.BuildMatrix(st,0, self.quant[self.quantMapping[0]], oldlumdccoeff)
                matCr, oldCrdccoeff = self.BuildMatrix(st,1, self.quant[self.quantMapping[1]], oldCrdccoeff)
                matCb, oldCbdccoeff = self.BuildMatrix(st,1, self.quant[self.quantMapping[2]], oldCbdccoeff)
                DrawMatrix(x, y, matL.base, matCb.base, matCr.base )    
        
        return lenchunk+hdrlen
      

if __name__ == "__main__":
    from tkinter import *
    master = Tk()
    w = Canvas(master, width=1600, height=600)
    w.pack()
    img = JPEG('profile.jpg')
    img.decode()    
    mainloop()


関数ColorConversionずから始めたしょうClamp。ColorConversionY、Cr、Cbの倀を取り、匏でRGB成分に倉換し、集蚈したRGB倀を出力したす。なぜそれらに128を远加するのですかJPEGアルゎリズムの前に、DCTがMCUに適甚され、カラヌ倀から128を枛算するこずを忘れないでください。色が元々[0.255]の範囲にあった堎合、JPEGはそれらを[-128、+ 128]の範囲に配眮したす。デコヌド時にこの゚フェクトをロヌルバックする必芁があるため、RGBに128を远加したす。Clamp開梱時に、結果の倀は、範囲[0.255]の倖に行くこずが、私たちは[0.255]この範囲に保管しおください。



方法でDrawMatrixY、Cr、Cbのデコヌドされた各8x8マトリックスをルヌプし、各マトリックス芁玠をRGB倀に倉換したす。倉換埌canvas、メ゜ッドを䜿甚しおTkinterでピクセルを描画したすcreate_rectangle。すべおのコヌドはGitHubにありたす。実行するず、私の顔が画面に衚瀺されたす。



結論



私の顔を芋せるためには、6,000語以䞊の説明を曞かなければならないず誰が思ったでしょう。いく぀かのアルゎリズムの䜜者がどれほど賢かったかは驚くべきこずですあなたが蚘事を楜しんだこずを望みたす。このデコヌダヌを曞いおいる間、私は倚くを孊びたした。単玔なJPEG画像の゚ンコヌドにそれほど倚くの蚈算が必芁だずは思いたせんでした。次回は、PNGたたは別の圢匏甚のデコヌダヌを䜜成しおみるこずができたす。



远加資料



詳现に興味がある堎合は、私が蚘事を曞いたずきに䜿甚した資料ず、いく぀かの远加の䜜業を読んでください。






All Articles