Programming lesson
Run-Length Encoding for Images: A Step-by-Step Tutorial with Pixel Art Examples
Learn run-length encoding (RLE) for image compression through hands-on examples with pixel art. Master encoding, decoding, hex conversion, and menu-driven programs in this comprehensive tutorial.
Introduction to Run-Length Encoding (RLE)
Run-length encoding (RLE) is a simple form of lossless data compression that is widely used in image processing, especially for pixel art and simple graphics. In RLE, consecutive repeated values (runs) are stored as a pair: the count of repetitions and the value itself. This technique is particularly effective for images with large areas of solid color, such as black-and-white sprites or retro game graphics.
In this tutorial, you'll learn how to implement RLE encoding and decoding for images, work with byte arrays, convert between data representations, and build a menu-driven program. The concepts covered include loops, strings, arrays, methods, and type-casting — all essential skills for programming assignments like the COP3504c Project 1.
Understanding RLE with Pixel Art
Imagine a row of pixels from a pixel art image of a gator: black (0) and green (2). The flat (unencoded) data might be: 0 0 2 2 2 0 0 0 0 0 0 2 2 0. Instead of storing each pixel individually, RLE records the run length and value: 2 0 3 2 6 0 2 2 1 0 (meaning two zeros, three twos, six zeros, two twos, one zero). This reduces storage for repetitive data.
In image formats, the first two bytes store the width and height. For example, an image of width 30 and height 20 would start with 1E 14 in hexadecimal. The remaining bytes encode the pixel runs.
Key Functions for RLE
To work with RLE, you need to implement several functions. Below are the essential ones, explained with examples.
1. count_runs(flatData)
This function returns the number of runs in flat (unencoded) data. For example, [15,15,15,4,4,4,4,4,4] has two runs: three 15s and six 4s, so it returns 2.
def count_runs(flat_data):
if not flat_data:
return 0
runs = 1
for i in range(1, len(flat_data)):
if flat_data[i] != flat_data[i-1]:
runs += 1
return runs
2. to_hex_string(data)
Converts a list of bytes (0-255) to a hexadecimal string without delimiters. For example, [3, 15, 6, 4] becomes "3f64".
def to_hex_string(data):
return ''.join(f'{x:02x}' for x in data)
3. encode_rle(flat_data)
Encodes flat data into RLE byte array. For [15,15,15,4,4,4,4,4,4], the result is [3, 15, 6, 4].
def encode_rle(flat_data):
if not flat_data:
return b''
encoded = []
count = 1
for i in range(1, len(flat_data)):
if flat_data[i] == flat_data[i-1] and count < 15:
count += 1
else:
encoded.append(count)
encoded.append(flat_data[i-1])
count = 1
encoded.append(count)
encoded.append(flat_data[-1])
return bytes(encoded)
4. get_decoded_length(rle_data)
Returns the total number of pixels after decoding. For [3, 15, 6, 4], it sums the run lengths: 3+6 = 9.
def get_decoded_length(rle_data):
return sum(rle_data[::2])
5. decode_rle(rle_data)
Decodes RLE data back to flat bytes. For [3, 15, 6, 4], the output is [15,15,15,4,4,4,4,4,4].
def decode_rle(rle_data):
decoded = []
for i in range(0, len(rle_data), 2):
count = rle_data[i]
value = rle_data[i+1]
decoded.extend([value] * count)
return bytes(decoded)
6. string_to_data(data_string)
Converts a hex string (like "3f64") into bytes. Inverse of to_hex_string.
def string_to_data(data_string):
return bytes.fromhex(data_string)
7. to_rle_string(rleData)
Converts RLE data to a human-readable string with decimal run lengths and hex values, separated by colons. Example: [10, 15, 6, 4] becomes "10f:64".
def to_rle_string(rle_data):
parts = []
for i in range(0, len(rle_data), 2):
parts.append(f"{rle_data[i]}{rle_data[i+1]:x}")
return ':'.join(parts)
Building the Menu-Driven Program
Your main program should display a menu with options to load data, display images, and convert between formats. The program uses the console_gfx module for image display. Here's a sample menu loop:
def main():
print("Welcome to RLE Image Processor!")
current_data = None
while True:
print("\nMenu:")
print("1. Load file")
print("2. Load test image")
print("3. Read RLE string")
print("4. Read RLE hex string")
print("5. Read flat hex string")
print("6. Display image")
print("7. Display RLE string")
print("8. Display RLE hex")
print("9. Display flat hex")
print("0. Exit")
choice = input("Select option: ")
if choice == '1':
filename = input("Enter filename: ")
current_data = console_gfx.load_file(filename)
elif choice == '2':
current_data = console_gfx.TEST_IMAGE
print("Test image loaded.")
elif choice == '3':
rle_str = input("Enter RLE string: ")
# parse and decode
parts = rle_str.split(':')
rle_bytes = []
for part in parts:
# part like "28" -> count=2, value=8
count = int(part[:-1])
value = int(part[-1], 16)
rle_bytes.extend([count, value])
current_data = decode_rle(rle_bytes)
elif choice == '4':
hex_str = input("Enter hex string: ")
rle_bytes = list(string_to_data(hex_str))
current_data = decode_rle(rle_bytes)
elif choice == '5':
hex_str = input("Enter hex string: ")
current_data = string_to_data(hex_str)
elif choice == '6':
if current_data:
console_gfx.display_image(current_data)
else:
print("No image loaded.")
elif choice == '7':
if current_data:
# encode current data to RLE
rle = encode_rle(current_data)
print("RLE representation:", to_rle_string(rle))
else:
print("No image loaded.")
elif choice == '8':
if current_data:
rle = encode_rle(current_data)
print("RLE hex values:", to_hex_string(rle))
else:
print("No image loaded.")
elif choice == '9':
if current_data:
print("Flat hex values:", to_hex_string(current_data))
else:
print("No image loaded.")
elif choice == '0':
break
else:
print("Invalid choice.")
Real-World Application: Retro Game Sprites
RLE is commonly used in retro video games to compress sprite data. For example, the classic game Super Mario Bros. uses RLE for its tile maps. In 2026, pixel art remains popular in indie games and even in some AI-generated art tools. Understanding RLE helps you appreciate how data compression works under the hood.
Common Pitfalls
- Run length limit: No run may exceed 15 pixels. If a run is longer, split it into multiple runs of 15.
- Data types: Ensure you use bytes (0-255) for pixel values, and that run counts are between 1 and 15.
- Hexadecimal conversion: Remember that hex values are case-insensitive, but consistency is key.
- Delimiters: When reading RLE strings, the delimiter is a colon (
:). Each part contains a decimal run length followed by a hex value (e.g.,28means count=2, value=8).
Practice Exercises
- Encode the flat data
[0,0,0,1,1,2,2,2,2]using RLE. - Decode the RLE bytes
[5, 10, 3, 7]. - Convert the hex string
"0a0b"to bytes. - Write a function that takes an RLE string like
"3f:2a"and returns the flat data.
Conclusion
Run-length encoding is a fundamental compression technique that is easy to implement and effective for certain types of data. By mastering these functions, you'll be well-prepared for the COP3504c project and gain insights into how images are stored and transmitted efficiently. Experiment with different pixel art images and see how RLE reduces their size!