File size: 6,891 Bytes
452e643
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""

CS5330 Fall 2024

Author: Calvin Lo

Lab 1: ASCII Art



This program generates the ASCII art representation of

an input image and compares the result to the original image

using SSIM.

"""

import gradio as gr
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
from skimage.metrics import structural_similarity as ssim

# ASCII characters to map pixel intensity
ASCII_CHARS = [
    ' ', '.', ',', ':', ';', 'i', 'l', '!', 'I', '1', 't', 'o', 'r', 'x', 'z', 'v', 
    'u', 'n', 'm', 'w', 'Q', 'B', 'N', 'M', '@'
]


def preprocess_image(image, new_width=150):
    """

    This function performs the following operations on the input image:

    -enhance contrast

    -perform edge detection (Sobel)

    -denoise edges

    -enhance contrast

    -resize image

    """
    # Open the image and convert to grayscale
    image = Image.fromarray(image)
    image = image.convert("L")

    # Contrast limited adaptive histogram equalization
    opencv_image = np.array(image)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    equalized_image = clahe.apply(opencv_image)

    # Apply Sobel edge detection
    sobelx = cv2.Sobel(equalized_image, cv2.CV_64F, 1, 0, ksize=5)
    sobely = cv2.Sobel(equalized_image, cv2.CV_64F, 0, 1, ksize=5)
    edges = np.hypot(sobelx, sobely)  # Combine gradients

    # Normalize the result to fit into the 0-255 grayscale range
    edges = np.uint8(255 * edges / np.max(edges))

    # Denoise the edges using Non-Local Means Denoising
    edges = cv2.fastNlMeansDenoising(edges, None, h=30, templateWindowSize=7, searchWindowSize=21)

    # Improve contrast of edges
    edges_image = Image.fromarray(edges)
    enhancer = ImageEnhance.Contrast(edges_image)
    enhanced_edges = enhancer.enhance(1.5)
    # enhanced_edges_array = np.array(enhanced_edges)

    # Invert the grayscale values of the original image
    inverted_image = 255 - np.array(equalized_image)

    # Convert sharpened edges to array for blending
    sharpened_edges_array = np.array(enhanced_edges )

    # Blend the inverted image with the sharpened edges using the blend_percentage
    blended_image = cv2.addWeighted(sharpened_edges_array, 1 - blend_percentage,
                                    inverted_image, blend_percentage, 0)

    # Resize the image for ASCII aspect ratio
    width, height = image.size
    aspect_ratio = height / width * 0.55  # Adjusting for ASCII aspect ratio
    new_height = int(aspect_ratio * new_width)
    resized_blended_image = Image.fromarray(blended_image).resize((new_width, new_height))

    return resized_blended_image


def image_to_ascii(image):
    """

    This function generates an ASCII representation of images.

    """
    pixels = np.array(image)
    ascii_str = ""
    for row in pixels:
        for pixel in row:
            # Mapping pixel intensity to ASCII
            ascii_str += ASCII_CHARS[pixel // 10]
        ascii_str += "\n"
    return ascii_str


def ascii_to_image(ascii_art, font_size=12, output_file='ascii_art.png'):
    """"

    This function converts ASCII text art to a png image.

    """

    # Specify font
    font_path = "DejaVuSansMono.ttf"
    font = ImageFont.truetype(font_path, font_size)

    # Get the required image dimensions
    lines = ascii_art.splitlines()
    max_width = max(font.getbbox(line)[2] for line in lines)  # Get max width
    img_height = len(lines) * font_size

    # Generate image with white background
    image = Image.new("RGB", (max_width, img_height), "white")
    draw = ImageDraw.Draw(image)

    # Draw each character from the ASCII text on the image
    for y, line in enumerate(lines):
        draw.text((0, y * font_size), line, fill="black", font=font)

    image.save(output_file)
    return image


def compare_ssim(image1, image2):
    """

    This function generates the structural similarity index

    measure for the two input images.

    """
    # Convert original color image to grayscale
    image1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
    
    # Convert ASCII image to grayscale and cv2 format
    image2 = np.array(image2.convert("L"))

    # Resize so both images have the same dimensions.
    image2 = cv2.resize(image2, (image1.shape[1], image1.shape[0]))

    score, _ = ssim(image1, image2, full=True)
    return score


def process_image(image):
    """

    This function takes an image and generates:

        -ASCII art as a string

        -ASCII text file

        -Compute SSIM for the ASCII art image and the original image

    """
    # Get image edges and convert to ASCII
    edges = preprocess_image(image)
    ascii_art = image_to_ascii(edges)

    # Convert ASCII art back to an image for SSIM calculation
    ascii_image = ascii_to_image(ascii_art)

    # Calculate SSIM
    ssim_score = compare_ssim(image, ascii_image)

    # Write ASCII art to txt file
    ascii_txt_path = "ascii_art_output.txt"
    with open(ascii_txt_path, "w") as f:
        f.write(ascii_art)

    return ascii_art, ssim_score, ascii_txt_path


def gradio_interface(image):
    """

    This function generates the interface outputs for a given input image.

    The outputs include the formatted SSIM, ASCII art, and ASCII art txt file path.

    """
    ascii_art, ssim_score, ascii_txt_path = process_image(image)
    ssim_formatted = f'<div style="font-size: 20px;">The structural similarity index measure is {ssim_score:.3f}</div>'
    # Wrap the ASCII art in HTML with custom font size
    styled_ascii_art = f'<pre style="font-family: monospace; font-size: 7px;">{ascii_art}</pre>'

    return styled_ascii_art, ssim_formatted, ascii_txt_path


def main():
    # Setup Gradio interface
    with gr.Blocks() as interface:
        # Add title and description
        gr.Markdown("<h1 style='font-size: 40px;'>Image to ASCII Art Converter</h1>")
        gr.Markdown("<p style='font-size: 18px;'>Upload an image, and this tool will generate an ASCII art version of the image. It also calculates the Structural Similarity Index (SSIM) to evaluate the quality.</p>")

        # Image input and ASCII art + SSIM score output
        image_input = gr.Image(label="Upload an image")
        ascii_art_output = gr.HTML()
        ssim_score_output = gr.HTML()

        # File output for downloading, initially hidden
        ascii_file_output = gr.File(label="Download ASCII Art")

        # Update interface when image is uploaded
        image_input.change(fn=gradio_interface,
                           inputs=image_input,
                           outputs=[ascii_art_output, ssim_score_output, ascii_file_output])

    # Launch interface
    interface.launch()


if __name__ == "__main__":
    main()