Hardware Generator for JPEG Compression
方榮彬, 龔祐萱
GitHub
Introduction
What is JPEG Compression ?
JPEG compression is a widely used image compression standard designed to reduce the file size of digital images while preserving visual quality. It employs a lossy compression technique, which removes redundant and less noticeable information to achieve high compression ratios. The process involves several key steps: color space transformation, Discrete Cosine Transform (DCT
) to separate image frequencies, quantization to reduce precision, and Huffman coding to encode the data efficiently. This makes JPEG ideal for applications where storage and bandwidth are critical, such as photography, web graphics, and mobile devices, while maintaining a balance between image quality and file size.
JPEG compression workflow
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
- Input image
- The input is typically an 8-bit grayscale or 24-bit RGB image.
- If the image is in RGB, it is converted to the YCbCr
- Y: Luminance (brightness)
- Cb: Chrominance (blue-difference)
- Cr: Chrominance (red-difference)
- Downsampling (Optional)
- Chrominance components (Cb and Cr) are typically downsampled because the human eye is less sensitive to color differences than to luminance changes.
- Common downsampling ratios:
- 4:4:4: No downsampling (full resolution for Y, Cb, and Cr).
- 4:2:2: Horizontal downsampling by 2.
- 4:2:0: Horizontal and vertical downsampling by 2.
- Block Splitting
- The image is divided into 8x8 pixel blocks for each channel (Y, Cb, Cr).
- Each block is processed independently in the subsequent steps.
- Level Shift
- Each pixel value in the 8x8 block is level-shifted by subtracting 128, bringing the range from [0, 255] to [-128, 127].
- This centers the values around zero for more efficient transformation.
- Discrete Cosine Transform (DCT)
- The DCT is applied to each 8x8 block, transforming spatial domain data into frequency domain data:
where:
- Quantization
- The DCT coefficients are divided by a quantization table and rounded to the nearest integer.
- Quantization reduces less visually significant high-frequency components to zero, achieving compression.
-
Zig-zag scanning
The quantized 8x8 block is converted into a 1D sequence using a zigzag pattern
-
Differential Pulse Code Modulation (DPCM)
Calculate the difference between the DC values of adjacent blocks to reduce redundant information
-
Run-Length Encoding (RLE)
- The 1D sequence is compressed using RLE, which encodes sequences of zero coefficients as a single value.
- Special markers:
- EOB (End of Block): Indicates no more non-zero values in the block.
- ZRL (Zero Run Length): Encodes runs of zeros.
- Huffman coding
Huffman coding is used to compress the data efficiently by assigning shorter codes to more frequent values and longer codes to less frequent values. This is particularly effective for the DC difference
and AC coefficients
.
Steps for Huffman Coding:
a. DC Component Encoding:
- The DC difference calculated using DPCM is represented as a pair:
- Size: The number of bits required to encode the amplitude of the DC difference.
- Amplitude: The actual bits of the DC difference.
- The Size is Huffman encoded using a predefined DC Huffman table.
b. AC Component Encoding:
- The AC coefficients are encoded using a combination of:
- Run/Size Pair: Encodes the number of consecutive zero coefficients (Run) followed by the size of the next non-zero coefficient.
- Amplitude: The actual value of the non-zero coefficient.
- Special markers:
- EOB (End of Block): Indicates that all remaining coefficients in the block are zero.
- ZRL (Zero Run Length): Encodes runs of 16 consecutive zeros.
c. Huffman Tables:
- Separate Huffman tables are used for DC and AC components.
- Separate tables are maintained for luminance and chrominance.
Implementation
The Python script utils.py
is designed to convert images of formats like *.png
or *.jpg
into the *.bmp
format, which is compatible with further processing in Chisel.
By running the below command
This converts {input}.jpg
into {output}.bmp
at maximum quality. However, we need to convert the image from ``RGB space to YCbCr
space, and store each channel in a separate file using the following function.
And then we will spilt the image to 8x8
block to maintain the compatibility.
1. Read the image in Chisel
In the hardware design, we read the image data through a test harness that loads preprocessed 8x8 blocks from text files:
- Data loading stucture
- Test Interface
The hardware receives image data as 8x8 blocks through its input interface, where each pixel value is represented as a signed integer after preprocessing. The input interface supports three color components (Y, Cb, Cr).
2. Zigzag scanning
We discovered issues in the original zigzag implementation and made the following improvements:
- Original Implementation Problems:
- Counter increment timing was incorrect: count was incremented at the start of processing state
- Register reset values were not properly managed
- State transitions were not optimally controlled
- Key Improvement
3.1 Run-Length Encode
The module uses a state machine to traverse the input data and generate a compressed output. The RLE algorithm identifies sequences of identical elements (referred to as "runs") in the input and encodes them as a pair of values:
- The count of consecutive identical elements (the run length).
- The value being repeated.
When we finish the test, we can obtain the compressed data in 1d
sequence, where the even index (0-indexed
) is for the number of identical number and the odd index is for the value. The format is well-suited for doing huffman coding in sw.
3.2 Differential Pulse Code Modulation
The original implementation had limitations in following the JPEG standard. Here's how we improved it:
Key improvements:
- Added storage for previous block's DC value
- Implemented true DPCM by computing difference between consecutive blocks' DC values
- First block automatically handled by initializing prevTopLeft to 0
- Maintained proper sequential processing for AC coefficients
This implementation now correctly follows the JPEG standard's DPCM encoding for DC coefficients.
4. Huffman coding
Our implementation of Huffman coding is shown below:
- Frequency calculation
- Goal: Determine how often each value appears in the encoded data.
- Process:
- Extract all values from the encoded data (
RLE
or Delta
) across all blocks and channels (Y
, Cb
, Cr
).
- Count the occurrences of each value using a frequency dictionary
- This frequency table will serve as the input for the Huffman tree construction.
- Huffman Tree Construction
- Goal: Build a binary tree where the most frequent symbols are closer to the root, resulting in shorter codes.
- Process:
- Create a priority queue (min-heap) using the
heapq
module, where each node is an instance of HuffmanNode
, which stores:
char
: The symbol (e.g., a value from RLE or Delta encoding).
freq
: The frequency of the symbol.
left
and right
: Pointers to child nodes.
- Combine two nodes with the smallest frequencies into a new internal node until only one node remains, forming the root of the tree.
- Generate Huffman Codes
- Goal: Assign binary codes to each symbol based on the structure of the Huffman tree.
- Process:
- Traverse the Huffman tree recursively:
- Append
0
when moving to the left child.
- Append
1
when moving to the right child.
- Assign the resulting binary string to each leaf node's symbol.
- Encode Data
- Goal: Replace each value in the input data with its corresponding Huffman code.
- Process:
- For each block in the encoded data, replace each value using the huffman_codes dictionary
- Save Results
- Huffman codes
Save the symbol-to-code mappings to a text file for later use or debugging
- Encoded Data
Save the encoded data (now in binary) to another text file
5. Bit-stream
The bitstream is structured to contain both encoding types (RLE and Delta) for each color component. For each component (Y, Cb, Cr), the bitstream is organized as follows:
The resulting bitstream format:
- Component Identifier (1 byte)
- DC Coefficient Difference (2 bytes)
- Sequence of RLE pairs:
- Run Length (1 byte)
- Value (2 bytes)
Each component's data is stored in a separate binary file under the hw_output/bitstream
directory.
After implementing our Huffman encoding process, we can analyze the efficiency of the compression:
For RLE (Run-Length Encoding) results:
- Y component uses 15 unique symbols with code lengths ranging from 1 to 6 bits
- Cb component needs only 5 unique symbols with more compact code lengths (2-3 bits)
- Cr component has 6 unique symbols utilizing 1-4 bits per code
These statistics demonstrate the data characteristics of each color component after DCT and quantization, with Y (luminance) requiring more diverse encoding patterns than the chrominance components (Cb, Cr).
The final compression results show:
- Original size: 192 bytes
- Compressed size: 138 bytes
- Achieving a compression ratio of 1.39:1
This creates a clear flow from our compression statistics to the actual bitstream structure.
The purpose of the IDCT module is to transform frequency domain data into spatial domain data. This module employs a state machine to manage and transition between the corresponding operations.
-
Waiting State
This is the default initial state, indicating that the module is waiting for a valid input.
When io.in.valid
is true
, the program will store the 8x8 matrix (io.in.bits.matrixIn
) into the register dctInput
and switch the state to calculating
. Then, it will set the output validity flag (validOut
) to false
, indicating that no valid output is ready yet.
-
Calculating State
This is the critical state where the Inverse Discrete Consine Transform (IDCT) computation is performed. The IDCT
function is invoked to compute each element of the matrix using the mathematical IDCT formula.
To enhance the precision of the calculations, intermediate values—such as the cosine coefficients and the scaling factors (alphaU
and alphaV
)—are multiplied by 100. This scaling helps mitigate precision loss during computation. Subsequently, The program divides the accumulated values by 1,000,000
to revert the scaling effect.
Once the IDCT computation is complete, the operation adds 128
to each element of the matrix to restore it to its original value range. The final result is stored in the register matrixOutput
, and the machine state is transitioned back to the waiting
state.
The complete program is as follows:
Encoder
Our hardware encoder workflow is listed below ( from 1. to 4. ):
- DCT Module
- Performs 2D Discrete Cosine Transform
- Converts spatial domain data to frequency domain
- Processes 8x8 blocks of pixels
- Quantization Module
- Reduces precision of DCT coefficients
- Uses configurable quantization tables
- Implements lossy compression step
- ZigZag Module
- Reorders 2D matrix into 1D array
- Groups similar frequencies together
- Prepares data for entropy encoding
- Encoding Stage
In the Encoding stage, we need to distinguish two kinds of coding type, one is RLE
and the other is DPCM
.
RLE
(Run-Length Encoding): Compresses sequences of repeated values
- Delta Encoding (
DPCM
): Encodes differences between consecutive values
Decoder
Our hardware decoder workflow is listed below (from 1. to 4.):
- Decoding Stage
RLE
(Run-Length Encoding): Decompresses sequences of repeated values
- Delta decoding (
DPCM
): Decodes differences between consecutive values
- Inverse ZigZag Module
- Reorders 1D array into 2D matrix
- Prepares data for inverse quantization module
- Inverse Quantization Module
- Uses configurable quantization tables
- Implements the reverse of the lossy compression step
- Inverse DCT Module
- Performs 2D Inverse Discrete Cosine Transform
- Converts frequency domain to spatial domain
- Processes 8x8 blocks of pixels
Test framework
When running the command below, the program will first execute the hardware
implementation and then verify the results against the software
reference model:
The output contains two main parts:
- Hardware execution output: Results from JPEG encoder hardware implementation
- Software validation results: Comparison between hardware and software model outputs for each stage (DCT, Quantization, ZigZag) of each color component (Y, Cb, Cr)
The code below shows our test framework that automates the hardware and software verification process:
import numpy as np
import subprocess
import time
from test import *
class JPEGDCT:
def __init__(self, scaling_factor=100):
self.scaling_factor = scaling_factor
def process_block(self, block):
N = 8
shifted_block = np.array(block, dtype=np.int32) - 128
dct_block = np.zeros((N, N), dtype=np.int64)
for u in range(N):
for v in range(N):
sum_val = 0
for i in range(N):
for j in range(N):
pixel_value = shifted_block[i][j]
cos_i = int(np.cos((2 * i + 1) * u * np.pi / 16) * 100)
cos_j = int(np.cos((2 * j + 1) * v * np.pi / 16) * 100)
cos_val = (pixel_value * cos_i) // 100
sum_val += cos_val * cos_j
alpha_u = int((1.0 / np.sqrt(2)) * 100) if u == 0 else 100
alpha_v = int((1.0 / np.sqrt(2)) * 100) if v == 0 else 100
intermediate = (sum_val * alpha_u) // 100
final = (intermediate * alpha_v) // 100
dct_block[u][v] = final // 4
return dct_block
class JPEGQuantization:
def __init__(self, qt_choice=1):
self.qt_choice = qt_choice
if qt_choice == 1:
self.quant_table = np.array([
[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]
])
else:
self.quant_table = np.array([
[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 quantize(self, dct_block):
"""Quantize the DCT coefficients"""
return np.round(dct_block / self.quant_table).astype(np.int32)
class JPEGZigzag:
def __init__(self):
self.zigzag_order = [
(0,0), (0,1), (1,0), (2,0), (1,1), (0,2), (0,3), (1,2),
(2,1), (3,0), (4,0), (3,1), (2,2), (1,3), (0,4), (0,5),
(1,4), (2,3), (3,2), (4,1), (5,0), (6,0), (5,1), (4,2),
(3,3), (2,4), (1,5), (0,6), (0,7), (1,6), (2,5), (3,4),
(4,3), (5,2), (6,1), (7,0), (7,1), (6,2), (5,3), (4,4),
(3,5), (2,6), (1,7), (2,7), (3,6), (4,5), (5,4), (6,3),
(7,2), (7,3), (6,4), (5,5), (4,6), (3,7), (4,7), (5,6),
(6,5), (7,4), (7,5), (6,6), (5,7), (6,7), (7,6), (7,7)
]
def scan(self, block):
"""Convert block to 1D array in zigzag order"""
return [block[i][j] for i, j in self.zigzag_order]
class JPEGTest:
@staticmethod
def read_block(filename):
"""Read an 8x8 block from file"""
with open(filename, 'r') as f:
values = [int(line.strip()) for line in f]
return np.array(values).reshape(8, 8)
def test_dct(self, input_file):
"""Test DCT processing"""
print(f"Testing DCT for {input_file}")
block = self.read_block(input_file)
dct = JPEGDCT()
result = dct.process_block(block)
print("\n=== DCT Output ===")
for row in result:
print(" ".join(f"{val:5d}" for val in row))
return result
def test_quantization(self, dct_result, qt_choice):
"""Test quantization processing"""
print("\n=== Quantization Output ===")
quant = JPEGQuantization(qt_choice)
quant_result = quant.quantize(dct_result)
for row in quant_result:
print(" ".join(f"{val:5d}" for val in row))
return quant_result
def test_zigzag(self, quant_result):
"""Test zigzag scanning"""
print("\n=== Zigzag Output ===")
zigzag = JPEGZigzag()
zigzag_result = zigzag.scan(quant_result)
for i, value in enumerate(zigzag_result):
print(f"Index {i}: {value}")
return zigzag_result
def compare_results(hw_file, sw_result, stage_name, threshold=1000):
try:
hw_values = np.loadtxt(hw_file, dtype=np.int64)
if isinstance(sw_result, list):
sw_result = np.array(sw_result, dtype=np.int64)
else:
sw_result = sw_result.astype(np.int64)
if len(hw_values.shape) == 2:
hw_values = hw_values.flatten()
if len(sw_result.shape) == 2:
sw_result = sw_result.flatten()
if 'DCT' in stage_name:
hw_values = hw_values // 10000
if 'Quantization' in stage_name or 'Zigzag' in stage_name:
sw_result = sw_result // 10000
diff = np.abs(hw_values - sw_result)
max_diff = np.max(diff)
if max_diff < threshold:
print(f"Test {stage_name}: PASS")
return True
else:
print(f"Test {stage_name}: FAIL (max diff: {max_diff:.2f})")
print(f"Hardware range: [{np.min(hw_values)}, {np.max(hw_values)}]")
print(f"Software range: [{np.min(sw_result)}, {np.max(sw_result)}]")
return False
except Exception as e:
print(f"Test {stage_name}: ERROR - {str(e)}")
return False
def test_full_pipeline():
try:
print("\nRunning hardware test...")
hw_start_time = time.time()
result = subprocess.run(["python3", "test.py"], capture_output=True, text=True)
hw_time = time.time() - hw_start_time
print(f"Take {hw_time:.2f}s")
if result.returncode != 0:
print("\nHardware test Failed")
print(result.stderr)
return
else:
print("\nHardware test completed")
print("\n=== Hardware Test Output ===")
if result.stdout:
print(result.stdout)
if result.stderr:
print("Errors:", result.stderr)
print("===========================\n")
components = [
("Y", 1),
("Cb", 2),
("Cr", 2)
]
for comp, qt_choice in components:
print(f"\nTesting {comp} component:")
test = JPEGTest()
input_block = test.read_block(f"hw_output/{comp.lower()}_block_0.txt")
dct = JPEGDCT()
sw_dct = dct.process_block(input_block)
quant = JPEGQuantization(qt_choice=qt_choice)
sw_quant = quant.quantize(sw_dct)
zigzag = JPEGZigzag()
sw_zigzag = zigzag.scan(sw_quant)
compare_results(f"hw_output/chisel_dct_{comp}.txt", sw_dct, f"{comp}_DCT")
compare_results(f"hw_output/chisel_quant_{comp}.txt", sw_quant, f"{comp}_Quantization")
compare_results(f"hw_output/chisel_zigzag_{comp}.txt", sw_zigzag, f"{comp}_Zigzag")
except Exception as e:
print(f"Error during testing: {e}")
raise
def main():
test_full_pipeline()
def generate_comparison_report(input_name):
print(f"\n=== Comparison Report for {input_name} ===")
print("\nSoftware Output:")
print("\nHardware Output:")
print("\nDifference Analysis:")
if __name__ == "__main__":
main()
Verilator
Run the command below
Generate JPEGEncodeChisel.v
, JPEGEncodeChisel.fir
and JPEGEncodeChisel.anno.json
.
Run


Reference