Everything should be made as simple as possible, but not simpler. (Albert Einstein)

Thursday, January 30, 2014

Basic JPEG Compressing/Decompressing Simulation

Standard JPEG compression uses (1) 8x8 Discrete Cosine Transform, (2) quantization based on certain luminance + chrominance tables, and (3) entropy-encoding (Huffman coding).
Here, I'm using OpenCV (Python) to simulate DCT + quantization + IDCT, without Huffman coding.



jpeg.org
wikipedia/jpeg
opencv

Ref:
"Image and Video Compression for Multimedia Engineering", Yun Q.Shi, Huifang Sun, CRC Press, 2000.
"Digital Image Processing", Third Ed., Rafael C. Gonzalez, Richard E. Woods, Pearson Prentice Hall, 2008.

(Image courtesy of Yun Q.Shi, Huifang Sun, CRC Press, taken from the book referenced above.)

Basically in this simulation, an image (grayscale and indexed-color) is compressed (transformed by DCT, then quantized), then decompressed (inverse DCT). We can see that original and the result is almost the same.

By adding Huffman coding, it is the standard JPEG compression/decompression.

Image is divided into 8x8 blocks before being processed.
According to JPEG standard, it is recommended that any incomplete (not 8x8) MCUs be completed by replication of the right-most column and the bottom row of each component.

This is simulation code for grayscale image,

"""
Simulation of basic JPEG compression,
only DCT + quantization IDCT,
not including entropy-encoding/Huffman-coding
"""

# jpeggraysim.py
import cv2
import numpy as np

std_luminance_quant_tbl = [
  [16,  11,  10,  16,  24,  40,  51,  61],
  [12,  12,  14,  19,  26,  58,  60,  55],
  [14,  13,  16,  24,  40,  57,  69,  56],
  [14,  17,  22,  29,  51,  87,  80,  62],
  [18,  22,  37,  56,  68, 109, 103,  77],
  [24,  35,  55,  64,  81, 104, 113,  92],
  [49,  64,  78,  87, 103, 121, 120, 101],
  [72,  92,  95,  98, 112, 100, 103,  99]
]

def JPEG_DCT(imgfile):
    # 1 channel x 8 bits, grayscale
    img = cv2.imread(imgfile, cv2.CV_LOAD_IMAGE_GRAYSCALE)
    wndName = "original grayscale"
    cv2.namedWindow(wndName, cv2.WINDOW_AUTOSIZE)
    cv2.imshow(wndName, img)

    iHeight, iWidth = img.shape[:2]
    print "size:", iWidth, "x", iHeight

    # set size to multiply of 8
    if (iWidth % 8) != 0:
        filler = img[:,iWidth-1:]
        #print "width filler size", filler.shape
        for i in range(8 - (iWidth % 8)):
            img = np.append(img, filler, 1)

    if (iHeight % 8) != 0:
        filler = img[iHeight-1:,:]
        #print "height filler size", filler.shape
        for i in range(8 - (iHeight % 8)):
            img = np.append(img, filler, 0)

    iHeight, iWidth = img.shape[:2]
    print "new size:", iWidth, "x", iHeight

    # array as storage for DCT + quant.result
    img2 = np.empty(shape=(iHeight, iWidth))

    # FORWARD ----------------
    # do calc. for each 8x8 non-overlapping blocks
    for startY in range(0, iHeight, 8):
        for startX in range(0, iWidth, 8):
            block = img[startY:startY+8, startX:startX+8]

            # apply DCT for a block
            blockf = np.float32(block)     # float conversion
            
            dst = cv2.dct(blockf)          # dct
                
            # quantization of the DCT coefficients
            blockq = np.around(np.divide(dst, std_luminance_quant_tbl))
            blockq = np.multiply(blockq, std_luminance_quant_tbl)

            # store the result
            for y in range(8):
                for x in range(8):
                    img2[startY+y, startX+x] = blockq[y, x]


    # INVERSE ----------------
    for startY in range(0, iHeight, 8):
        for startX in range(0, iWidth, 8):
            block = img2[startY:startY+8, startX:startX+8]
            
            blockf = np.float32(block)     # float conversion
            dst = cv2.idct(blockf)         # inverse dct
            np.place(dst, dst>255.0, 255.0)     # saturation
            np.place(dst, dst<0.0, 0.0)         # grounding 
            block = np.uint8(np.around(dst))

            # store the results
            for y in range(8):
                for x in range(8):
                    img[startY+y, startX+x] = block[y, x]


    wndName1 = "DCT + quantization + inverseDCT"
    cv2.namedWindow(wndName1, cv2.WINDOW_AUTOSIZE)
    cv2.imshow(wndName1, img)
    cv2.waitKey(0)
    cv2.destroyWindow(wndName1)
    cv2.destroyWindow(wndName)

if __name__ == "__main__":
    JPEG_DCT("cat01.jpg")
    
Source image: 
After processing, DCT + quantization + IDCT: 

This is simulation code for indexed-color image,

"""
@Soesilo Wijono
Simulation of basic JPEG compression,
only DCT + quantization + IDCT,
not including entropy-encoding/Huffman-coding
"""

# jpegcolorsim.py
#import sys
#import time
import cv2
import numpy as np

# table of [luminance, chrominance, chrominance]
std_quant_tbl = [
  [
  [16,  11,  10,  16,  24,  40,  51,  61],
  [12,  12,  14,  19,  26,  58,  60,  55],
  [14,  13,  16,  24,  40,  57,  69,  56],
  [14,  17,  22,  29,  51,  87,  80,  62],
  [18,  22,  37,  56,  68, 109, 103,  77],
  [24,  35,  55,  64,  81, 104, 113,  92],
  [49,  64,  78,  87, 103, 121, 120, 101],
  [72,  92,  95,  98, 112, 100, 103,  99]
  ],
  [
  [17,  18,  24,  47,  99,  99,  99,  99],
  [18,  21,  26,  66,  99,  99,  99,  99],
  [24,  26,  56,  99,  99,  99,  99,  99],
  [47,  66,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99]
  ],
  [
  [17,  18,  24,  47,  99,  99,  99,  99],
  [18,  21,  26,  66,  99,  99,  99,  99],
  [24,  26,  56,  99,  99,  99,  99,  99],
  [47,  66,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99],
  [99,  99,  99,  99,  99,  99,  99,  99]
  ]
]

def JPEG_DCT(imgfile):
    # 3 channels x 8 bits, indexed-colors
    img = cv2.imread(imgfile, cv2.CV_LOAD_IMAGE_COLOR)
    wndName = "original indexed-color"
    cv2.namedWindow(wndName, cv2.WINDOW_AUTOSIZE)
    cv2.imshow(wndName, img)

    iHeight, iWidth = img.shape[:2]
    print "size:", iWidth, "x", iHeight

    # set size to multiply of 8
    if (iWidth % 8) != 0:
        filler = img[:,iWidth-1:,:]
        #print "width filler size", filler.shape
        for i in range(8 - (iWidth % 8)):
            img = np.append(img, filler, 1)

    if (iHeight % 8) != 0:
        filler = img[iHeight-1:,:,:]
        #print "height filler size", filler.shape
        for i in range(8 - (iHeight % 8)):
            img = np.append(img, filler, 0)

    iHeight, iWidth = img.shape[:2]
    print "new size:", iWidth, "x", iHeight

    # convert to YCrCb, Y=luminance, Cr/Cb=red/blue-difference chrominance
    img = cv2.cvtColor(img, cv2.COLOR_BGR2YCR_CB)

    # array as storage for DCT + quant.result
    img2 = np.empty(shape=(iHeight, iWidth, 3))

    # FORWARD ----------------
    # do calc. for each 8x8 non-overlapping blocks
    for startY in range(0, iHeight, 8):
        for startX in range(0, iWidth, 8):
            for c in range(0, 3):
                block = img[startY:startY+8, startX:startX+8, c:c+1].reshape(8,8)

                # apply DCT for a block
                blockf = np.float32(block)     # float conversion
                dst = cv2.dct(blockf)          # dct
                  
                # quantization of the DCT coefficients
                blockq = np.around(np.divide(dst, std_quant_tbl[c]))
                blockq = np.multiply(blockq, std_quant_tbl[c])

                # store the result
                for y in range(8):
                    for x in range(8):
                        img2[startY+y, startX+x, c] = blockq[y, x]


    # INVERSE ----------------
    for startY in range(0, iHeight, 8):
        for startX in range(0, iWidth, 8):
            for c in range(0, 3):
                block = img2[startY:startY+8, startX:startX+8, c:c+1].reshape(8,8)
               
                blockf = np.float32(block)     # float conversion
                dst = cv2.idct(blockf)         # inverse dct
                np.place(dst, dst>255.0, 255.0)     # saturation
                np.place(dst, dst<0.0, 0.0)         # grounding 
                block = np.uint8(np.around(dst)) 

                # store the results
                for y in range(8):
                    for x in range(8):
                        img[startY+y, startX+x, c] = block[y, x]

    # convert to BGR
    img = cv2.cvtColor(img, cv2.COLOR_YCR_CB2BGR)
    
    wndName1 = "DCT + quantization + inverseDCT"
    cv2.namedWindow(wndName1, cv2.WINDOW_AUTOSIZE)
    cv2.imshow(wndName1, img)
    cv2.waitKey(0)
    cv2.destroyWindow(wndName1)
    cv2.destroyWindow(wndName)

if __name__ == "__main__":
    JPEG_DCT("cat02.png")
    

Source Image:

After processing, DCT + quantization + IDCT: