Programming lesson
Run-Length Encoding in Python: A Tutorial for COP3502C HW 3 & 4
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 examples inspired by pixel art and gaming trends.
Introduction to Run-Length Encoding (RLE) in Python
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 this tutorial, we will explore how to implement RLE encoding and decoding in Python, focusing on the requirements of COP3502C Homework 3 and 4. By the end, you will be able to write functions that convert between raw pixel data and RLE representations, display images, and build a menu-driven program.
RLE works by replacing sequences of identical values (runs) with a count and the value. For example, the flat data [15, 15, 15, 4, 4, 4, 4, 4, 4] can be encoded as [3, 15, 6, 4], meaning three 15s followed by six 4s. This is especially useful for images with large areas of uniform color, like the pixel art seen in retro games or modern indie titles. With the rise of AI-generated art and pixel-style games, understanding RLE is more relevant than ever.
Understanding the Assignment Context
In COP3502C, you will work with images stored as lists of numbers. The first two numbers represent the width and height of the image. Each pixel is a value between 0 and 15 (representing 16 colors). Your task is to implement functions that encode raw data into RLE, decode RLE back to raw, and convert between different string representations (hexadecimal and decimal). You will also create a menu-driven program that allows users to load data, display images, and view RLE strings.
This assignment gives you practice with loops, lists, string manipulation, and type casting. It's a great way to solidify your Python skills while working on a real-world compression technique.
Key Functions to Implement
Let's break down the required functions step by step. We'll implement them in the order suggested by the assignment.
1. to_hex_string(data)
This function converts a list of integers (0-15) into a hexadecimal string without delimiters. For example, [3, 15, 6, 4] becomes "3f64". Use Python's hex() function or a custom mapping.
def to_hex_string(data):
return ''.join(f'{x:x}' for x in data)
This is useful for debugging and for displaying data in a compact format. It's also the inverse of string_to_data.
2. count_runs(flat_data)
Count the number of runs in flat data. A run is a sequence of identical consecutive values. For [15, 15, 15, 4, 4, 4, 4, 4, 4], there are two runs (three 15s and six 4s), so the function returns 2. This is useful for determining the size of the encoded list.
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
3. encode_rle(flat_data)
This is the core encoding function. It returns a list in RLE format: for each run, append the count and then the value. For example, encode_rle([15,15,15,4,4,4,4,4,4]) returns [3,15,6,4].
def encode_rle(flat_data):
if not flat_data:
return []
rle = []
count = 1
prev = flat_data[0]
for i in range(1, len(flat_data)):
if flat_data[i] == prev:
count += 1
else:
rle.append(count)
rle.append(prev)
prev = flat_data[i]
count = 1
rle.append(count)
rle.append(prev)
return rle
This function is the heart of the assignment. Think of it like compressing a repeating pattern in a game level or a meme image.
4. get_decoded_length(rle_data)
Given RLE data, return the total number of pixels after decoding. For [3,15,6,4], the decoded length is 3+6 = 9. This is the inverse of count_runs.
def get_decoded_length(rle_data):
return sum(rle_data[::2]) # sum of counts at even indices
5. decode_rle(rle_data)
Decode RLE data back to flat data. For each pair (count, value), repeat the value count times.
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
This is like decompressing a sprite sheet for a game character.
6. string_to_data(data_string)
Convert a hexadecimal string (e.g., "3f64") into a list of integers. Each two-character hex pair becomes one integer? Actually, in the assignment, each character represents a nibble (0-15). So "3f64" becomes [3, 15, 6, 4].
def string_to_data(data_string):
return [int(ch, 16) for ch in data_string]
7. to_rle_string(rle_data)
Convert RLE data to a human-readable string: for each run, the decimal count followed by the hex value, separated by colons. Example: [15, 15, 6, 4] becomes "15f:64".
def to_rle_string(rle_data):
parts = []
for i in range(0, len(rle_data), 2):
count = rle_data[i]
value = rle_data[i+1]
parts.append(f"{count:x}:{value:x}")
return ':'.join(parts)
Note: The assignment uses decimal for count and hex for value. In our example, count 15 is decimal, value 15 is hex 'f'. So the string is "15f:64".
8. string_to_rle(rle_string)
Inverse of the above: parse a string like "15f:64" into RLE list [15, 15, 6, 4].
def string_to_rle(rle_string):
rle = []
for part in rle_string.split(':'):
count = int(part[:-1], 16) # all but last char are decimal digits? Wait, "15f" means count=15 (decimal), value=f (hex). So count is the part before the last character, but careful: count can be 1-2 digits. Simpler: parse as two parts: count_str = part[:-1], value_str = part[-1].
count_str = part[:-1]
value_str = part[-1]
rle.append(int(count_str))
rle.append(int(value_str, 16))
return rle
Make sure to handle cases where count is one or two digits. This function is critical for reading user input.
Building the Menu-Driven Program
Your program should run in standalone mode with a menu. Here's a skeleton:
import ConsoleGfx # provided by instructor
def main():
print("Welcome to RLE Image Processor!")
ConsoleGfx.test_rainbow()
image_data = None
while True:
print_menu()
choice = input("Select a Menu Option: ")
if choice == '1':
filename = input("Enter name of file to load: ")
image_data = ConsoleGfx.load_file(filename)
elif choice == '2':
image_data = ConsoleGfx.test_image
print("Test image data loaded.")
# ... other options
elif choice == '6':
ConsoleGfx.display_image(image_data)
# ... etc.
Remember to handle the case where no image is loaded before displaying.
Connecting to Trends: Gaming and Pixel Art
RLE is not just an academic exercise. It's used in many retro game consoles like the NES and Game Boy to compress sprite data. In modern times, pixel art games like Minecraft and Stardew Valley use similar techniques to store textures efficiently. Even AI-generated pixel art tools leverage RLE for quick rendering. As of May 2026, the popularity of AI-assisted game development continues to grow, making compression skills valuable for indie developers.
Think about encoding a simple sprite: a 16x16 character with a lot of transparent or solid color blocks. RLE can reduce the data size significantly. For example, a row of 16 black pixels (value 0) can be encoded as [16, 0] instead of 16 individual zeros.
Common Pitfalls and Tips
- Off-by-one errors: Ensure your loops correctly handle the start and end of data.
- Hex vs decimal: Always double-check whether a value is in hex or decimal. The assignment specifies: run lengths are decimal, pixel values are hex in string representations.
- Delimiters: In
to_rle_string, use colons between runs, but no trailing colon. - Edge cases: Empty data, single run, runs longer than 15? Actually, run length can be up to 127? The assignment doesn't specify a limit, but typical RLE uses counts up to 255. However, for this project, counts can be any integer within Python's list capacity.
Testing Your Functions
Use the examples from the assignment to test each function. For instance:
print(to_hex_string([3, 15, 6, 4])) # Expected: "3f64"
print(count_runs([15,15,15,4,4,4,4,4,4])) # Expected: 2
print(encode_rle([15,15,15,4,4,4,4,4,4])) # Expected: [3,15,6,4]
print(get_decoded_length([3,15,6,4])) # Expected: 9
print(decode_rle([3,15,6,4])) # Expected: [15,15,15,4,4,4,4,4,4]
print(string_to_data("3f64")) # Expected: [3,15,6,4]
print(to_rle_string([15,15,6,4])) # Expected: "15f:64"
print(string_to_rle("15f:64")) # Expected: [15,15,6,4]
Make sure your output matches exactly, as the assignment requires.
Conclusion
Run-length encoding is a fundamental compression technique that bridges theory and practice. By implementing these functions in Python, you gain hands-on experience with loops, list manipulation, and string formatting. Whether you're building a retro game or processing AI-generated art, these skills will serve you well. Good luck with your COP3502C assignments!