from PIL import Image, ImageDraw, ImageOps, ImageFilter, ImageFont import numpy as np import math import gradio as gr from skimage.metrics import structural_similarity as ssim import cv2 ##### CONSTANTS ##### # ASCII characters used to represent image pixels, reversed for better contrast in mapping CHARS = ' .:-=+*#%@'[::-1] # Convert the characters to a list for easier access CHAR_ARRAY = list(CHARS) # Number of available ASCII characters CHAR_LEN = len(CHAR_ARRAY) # Grayscale level for each ASCII character, determining how many shades of gray each character represents GRAYSCALE_LEVEL = CHAR_LEN / 256 # Target number of characters per row TARGET_WIDTH = 200 # Character dimensions (width and height in pixels) used to match image aspect ratio to character aspect ratio CHAR_W = 6 CHAR_H = 14 ##### ASCII ART GENERATION ##### def getChar(inputInt, gamma=1.8): """Map a grayscale pixel value to an ASCII character with gamma correction applied.""" # Adjust the input pixel intensity using gamma correction for perceptual brightness adjustment inputInt = (inputInt / 255) ** gamma * 255 # Map the corrected pixel value to an appropriate ASCII character return CHAR_ARRAY[math.floor(inputInt * GRAYSCALE_LEVEL)] def load_and_preprocess_image(image): """Resize and preprocess the input image, adjusting contrast and blurring for better ASCII conversion.""" width, height = image.size # Calculate the scaling factor to match the target character width scale_factor = TARGET_WIDTH / width # Resize the image, maintaining the aspect ratio considering the character dimensions new_width = TARGET_WIDTH new_height = int(scale_factor * height * (CHAR_W / CHAR_H)) # Adjust height to maintain aspect ratio # Resize the image based on the calculated dimensions im = image.resize((new_width, new_height)) # Enhance contrast to bring out more detail in the ASCII representation im = ImageOps.equalize(im, mask=None) # Apply a slight blur to reduce noise and simplify pixel values im = im.filter(ImageFilter.GaussianBlur(radius=0.5)) return im def create_ascii_art(im): """Convert a preprocessed image into ASCII art by mapping grayscale values to ASCII characters.""" # Convert the image to grayscale grayscale_image = im.convert("L") # Get the pixel values as a NumPy array for easier processing pix = np.array(grayscale_image) width, height = grayscale_image.size # Create a string that holds the ASCII art, where each character represents a pixel ascii_art = "" for i in range(height): for j in range(width): # Append the corresponding ASCII character for each pixel ascii_art += getChar(pix[i, j]) # Newline after each row to maintain image structure ascii_art += '\n' return ascii_art def draw_ascii_image(ascii_art_string, char_width, char_height, font_size): """Draw the ASCII art string onto an image.""" # Split the ASCII art string into lines lines = ascii_art_string.split('\n') # Determine the dimensions of the image based on the number of characters width = max(len(line) for line in lines) height = len(lines) # Create a blank white image based on the ASCII art size and font size img_width = width * char_width img_height = height * char_height ascii_image = Image.new("RGB", (img_width, img_height), "white") draw = ImageDraw.Draw(ascii_image) # Use default or custom font (Pillow's default font in this case) font = ImageFont.load_default() # Draw the ASCII art on the image for i, line in enumerate(lines): # Draw each line of the ASCII art onto the image draw.text((0, i * char_height), line, font=font, fill="black") return ascii_image ##### EVALUATION ##### def ascii_to_image(ascii_art, width, height): """Convert ASCII art back to an image by mapping characters to grayscale values.""" char_to_gray = {c: int((i / CHAR_LEN) * 255) for i, c in enumerate(CHAR_ARRAY)} ascii_lines = ascii_art.split('\n') # Create an empty numpy array to store the pixel values ascii_image = np.zeros((height, width), dtype=np.uint8) for i, line in enumerate(ascii_lines): if i >= height: break for j, char in enumerate(line): if j >= width: break ascii_image[i, j] = char_to_gray.get(char, 255) # Default to white if character not in mapping return Image.fromarray(ascii_image) def calculate_ssi(original_image, ascii_art_image): """Calculate Structural Similarity Index (SSI) between the original and reconstructed images.""" original_image = original_image.convert("L") # Resize the original image to match the dimensions of the ASCII art image original_image_resized = original_image.resize(ascii_art_image.size) # Convert both images to NumPy arrays original_array = np.array(original_image_resized) ascii_array = np.array(ascii_art_image) # Calculate SSI ssi_value, _ = ssim(original_array, ascii_array, full=True) return ssi_value ##### MAIN FUNCTION FOR GRADIO INTERFACE ##### def process_image(image): """Process the input image to generate both an ASCII art image and a downloadable text file.""" resized_image = load_and_preprocess_image(image) ascii_art = create_ascii_art(resized_image) output_image = draw_ascii_image(ascii_art, char_width=CHAR_W, char_height=CHAR_H, font_size=10) # Convert the ASCII art back into an image for SSI comparison ascii_art_image = ascii_to_image(ascii_art, resized_image.width, resized_image.height) # Calculate SSI between the original and ASCII art image ssi_value = calculate_ssi(image, ascii_art_image) # Save the ASCII art as a text file ascii_txt_path = "ascii_art.txt" with open(ascii_txt_path, "w") as text_file: text_file.write(ascii_art) print(f"Structural Similarity Index (SSI): {ssi_value}") return output_image, ascii_txt_path, ssi_value ##### GRADIO INTERFACE ##### def gradio_interface(image): ascii_image, txt_file, ssi_value = process_image(image) return ascii_image, txt_file, ssi_value demo = gr.Interface( fn=gradio_interface, inputs=gr.Image(type="pil", label="Upload an Image", height=300), outputs=[ gr.Image(type="pil", label="ASCII Art Image", height=300), gr.File(label="Download ASCII Art Text File", height=50), gr.Textbox(label="SSI Value") ], title="ASCII Art Generator with SSI Metric", description="Upload an image, generate ASCII art, and calculate the Structural Similarity Index (SSI).", allow_flagging="never", examples=[ ['images/building.jpg'], ['images/cat.webp'], ['images/dog.jpg'], ['images/people.jpg'], ['images/cartoon.png'], ['images/einstein.jpg'], ], ) demo.launch()