Programming lesson
Run-Length Encoding in Python: A Step-by-Step Guide for Image Compression
Learn how to implement run-length encoding (RLE) for image data in Python. This tutorial covers encoding, decoding, hex conversion, and menu-driven programs with practical examples.
Introduction to Run-Length Encoding (RLE)
Run-length encoding (RLE) is a simple lossless compression technique widely used in image processing, especially for pixel art and game sprites. Instead of storing every pixel individually, RLE stores runs of identical values as a pair: a count and the value. This drastically reduces storage for images with large blocks of uniform color, like the classic 8-bit game graphics from Mario or Pokémon.
In this tutorial, you'll learn how to implement RLE encoding and decoding in Python, convert between flat data and RLE representations, and build a menu-driven program similar to the one required in COP3502C assignments. We'll use timely examples inspired by the 2026 FIFA World Cup pixel art mascots and retro gaming trends.
Understanding the Data Format
Images are stored as a list of integers, where the first two numbers represent width and height. Each pixel value ranges from 0 to 15 (16 colors). For example, a simple 4x4 smiley face might have flat data: [4, 4, 0, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 0, 0, 4, 4] (first two are dimensions).
RLE encoding compresses this by grouping consecutive identical pixels. For instance, [0, 0, 0, 2, 2] becomes [3, 0, 2, 2] (three zeros followed by two twos).
Implementing Core Functions
We'll implement eight essential methods step by step.
1. to_hex_string(data)
Converts a list of integers (0-15) into a hexadecimal string without delimiters. Useful for debugging and display.
def to_hex_string(data):
return ''.join(f'{x:x}' for x in data)
# Example
print(to_hex_string([3, 15, 6, 4])) # Output: 3f642. count_runs(flat_data)
Counts the number of runs in flat data. Each run is a consecutive sequence of identical values.
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
# Example
print(count_runs([15, 15, 15, 4, 4, 4, 4, 4, 4])) # Output: 23. encode_rle(flat_data)
Encodes flat data into RLE format: alternating run length and value.
def encode_rle(flat_data):
if not flat_data:
return []
rle = []
count = 1
for i in range(1, len(flat_data)):
if flat_data[i] == flat_data[i-1]:
count += 1
else:
rle.extend([count, flat_data[i-1]])
count = 1
rle.extend([count, flat_data[-1]])
return rle
# Example
print(encode_rle([15, 15, 15, 4, 4, 4, 4, 4, 4])) # Output: [3, 15, 6, 4]4. get_decoded_length(rle_data)
Returns the total number of pixels after decoding (sum of all run lengths).
def get_decoded_length(rle_data):
return sum(rle_data[::2]) # Sum of all run lengths
# Example
print(get_decoded_length([3, 15, 6, 4])) # Output: 95. decode_rle(rle_data)
Decodes RLE data back to flat data.
def decode_rle(rle_data):
flat = []
for i in range(0, len(rle_data), 2):
count = rle_data[i]
value = rle_data[i+1]
flat.extend([value] * count)
return flat
# Example
print(decode_rle([3, 15, 6, 4])) # Output: [15, 15, 15, 4, 4, 4, 4, 4, 4]6. string_to_data(data_string)
Converts a hexadecimal string into a list of integers.
def string_to_data(data_string):
return [int(ch, 16) for ch in data_string]
# Example
print(string_to_data('3f64')) # Output: [3, 15, 6, 4]7. to_rle_string(rle_data)
Converts RLE data into human-readable format: run_length:run_value separated by colons.
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)
# Example
print(to_rle_string([15, 15, 6, 4])) # Output: 15f:648. string_to_rle(rle_string)
Parses a human-readable RLE string back to RLE data list.
def string_to_rle(rle_string):
rle = []
for part in rle_string.split(':'):
if len(part) == 2:
count = int(part[0])
value = int(part[1], 16)
else:
count = int(part[:-1])
value = int(part[-1], 16)
rle.extend([count, value])
return rle
# Example
print(string_to_rle('15f:64')) # Output: [15, 15, 6, 4]Building the Menu-Driven Program
Now combine these functions into a standalone program with a menu. The program loads data via file, test image, or user input, and displays results.
import ConsoleGfx # Provided module
def main():
image_data = None
print("Welcome to the RLE Image Processor!")
ConsoleGfx.test_rainbow()
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: ")
image_data = ConsoleGfx.load_file(filename)
print("File loaded.")
elif choice == '2':
image_data = ConsoleGfx.test_image
print("Test image loaded.")
elif choice == '3':
rle_str = input("Enter RLE string: ")
rle_data = string_to_rle(rle_str)
image_data = decode_rle(rle_data)
# Prepend dimensions? Typically dimensions are stored separately.
elif choice == '4':
hex_str = input("Enter hex RLE string: ")
rle_data = string_to_data(hex_str)
image_data = decode_rle(rle_data)
elif choice == '5':
hex_str = input("Enter hex flat data: ")
image_data = string_to_data(hex_str)
elif choice == '6':
if image_data:
ConsoleGfx.display_image(image_data)
else:
print("No image loaded.")
elif choice == '7':
if image_data:
rle = encode_rle(image_data[2:]) # Exclude dimensions
print("RLE representation:", to_rle_string(rle))
else:
print("No image loaded.")
elif choice == '8':
if image_data:
rle = encode_rle(image_data[2:])
print("RLE hex values:", to_hex_string(rle))
else:
print("No image loaded.")
elif choice == '9':
if image_data:
print("Flat hex values:", to_hex_string(image_data[2:]))
else:
print("No image loaded.")
elif choice == '0':
break
if __name__ == "__main__":
main()Testing with Real Examples
Use the chubby smiley data: width=8, height=8, flat hex: 880bbbbbb0bbbbbbbbbb0bb0bbbbbbbbbbbb0bb0bbbbb00bbbbbbbbbbb0bbbbbb0. Load this via option 5 and display the image. The RLE string should match: 28:10:6b:10:10b:10:2b:10:12b:10:2b:10:5b:20:11b:10:6b:10.
Trend Connection: Pixel Art in Modern Games
RLE is still used in indie games like Stardew Valley and retro-styled titles. With the rise of AI-generated pixel art and the 2026 World Cup's pixel mascots, understanding RLE helps you appreciate how game developers optimize assets. This assignment sharpens your Python skills in loops, lists, and string manipulation—essential for any aspiring developer.
Common Pitfalls and Tips
- Off-by-one errors: Ensure run counts are correct when encoding/decoding.
- Hex conversion: Remember that values are 0-15, not 0-255.
- Delimiter consistency: Use colon in to_rle_string and split by colon in string_to_rle.
- Test with provided examples: Match output exactly to avoid losing credit.
Conclusion
You've now implemented a complete RLE image compression toolkit in Python. These functions form the backbone of many real-world applications, from PNG compression to fax machines. Practice with different images and explore extending the program to handle larger color palettes. Good luck with your COP3502C assignment!