Last active
January 8, 2025 01:54
-
-
Save kalomaze/52d111a196456126b20ed97daabf24db to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import random | |
import numpy as np | |
import string | |
from datetime import datetime | |
from PIL import Image, ImageEnhance, ImageOps | |
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, | |
QHBoxLayout, QTextEdit, QPushButton, QCheckBox, | |
QLabel, QSpinBox, QComboBox, QSlider, QFileDialog, | |
QFrame) | |
from PyQt5.QtCore import Qt | |
from PyQt5.QtGui import QPixmap, QImage, QFont | |
import pyqtgraph as pg | |
class ASCIICorruptor(QMainWindow): | |
def __init__(self): | |
super().__init__() | |
self.setWindowTitle("ASCII Art Generator & Corruptor") | |
self.setGeometry(100, 100, 1400, 800) | |
# ASCII characters for different brightness levels | |
self.ASCII_CHARS = "@%#*+=-:. " | |
self.current_image = None | |
self.current_ascii = None | |
self.batch_images = [] | |
self.batch_ascii = [] # Add this line to initialize batch_ascii | |
self.init_ui() | |
def init_ui(self): | |
main_widget = QWidget() | |
self.setCentralWidget(main_widget) | |
layout = QHBoxLayout(main_widget) | |
# Left panel - Image and ASCII preview | |
left_panel = QWidget() | |
left_layout = QVBoxLayout(left_panel) | |
# Image preview | |
self.image_label = QLabel("Click to select image") | |
self.image_label.setFrameStyle(QFrame.Panel | QFrame.Sunken) | |
self.image_label.setMinimumSize(300, 300) | |
self.image_label.setAlignment(Qt.AlignCenter) | |
self.image_label.mousePressEvent = self.load_image | |
# ASCII preview | |
self.ascii_preview = QTextEdit() | |
self.ascii_preview.setFont(QFont("Courier", 8)) | |
self.ascii_preview.setReadOnly(True) | |
left_layout.addWidget(self.image_label) | |
left_layout.addWidget(self.ascii_preview) | |
# Middle panel - Controls | |
middle_panel = QWidget() | |
middle_layout = QVBoxLayout(middle_panel) | |
# Image controls | |
image_controls = QWidget() | |
image_layout = QVBoxLayout(image_controls) | |
# Sliders | |
self.sliders = {} | |
slider_configs = { | |
'Max Characters': (10, 200, 100), | |
'Brightness': (0, 200, 100), | |
'Contrast': (0, 200, 100), | |
'Saturation': (0, 200, 100), | |
} | |
for name, (min_val, max_val, default) in slider_configs.items(): | |
slider_widget = QWidget() | |
slider_layout = QHBoxLayout(slider_widget) | |
slider_layout.addWidget(QLabel(f"{name}:")) | |
slider = QSlider(Qt.Horizontal) | |
slider.setMinimum(min_val) | |
slider.setMaximum(max_val) | |
slider.setValue(default) | |
slider.valueChanged.connect(self.update_ascii) | |
self.sliders[name] = slider | |
slider_layout.addWidget(slider) | |
image_layout.addWidget(slider_widget) | |
# Corruption controls | |
corruption_controls = QWidget() | |
corruption_layout = QVBoxLayout(corruption_controls) | |
# Corruption pattern selector | |
self.pattern_type = QComboBox() | |
self.pattern_type.addItems(['Constant', 'Gaussian', 'Linear Descent']) | |
corruption_layout.addWidget(QLabel("Corruption Pattern:")) | |
corruption_layout.addWidget(self.pattern_type) | |
# Growth curve selector | |
self.curve_type = QComboBox() | |
self.curve_type.addItems(['Linear', 'Exponential', 'Logarithmic', 'Sigmoid', 'Square Root']) | |
corruption_layout.addWidget(QLabel("Growth Curve:")) | |
corruption_layout.addWidget(self.curve_type) | |
# Number of corruption steps | |
steps_widget = QWidget() | |
steps_layout = QHBoxLayout(steps_widget) | |
steps_layout.addWidget(QLabel("Corruption Steps:")) | |
self.num_steps = QSpinBox() | |
self.num_steps.setValue(8) | |
self.num_steps.setRange(2, 64) | |
steps_layout.addWidget(self.num_steps) | |
corruption_layout.addWidget(steps_widget) | |
# Add scaling controls | |
scaling_widget = QWidget() | |
scaling_layout = QHBoxLayout(scaling_widget) | |
self.enable_scaling = QCheckBox("Enable Size Scaling") | |
scaling_layout.addWidget(self.enable_scaling) | |
self.scale_factor = QSpinBox() | |
self.scale_factor.setRange(110, 200) | |
self.scale_factor.setValue(150) | |
self.scale_factor.setSuffix("%") | |
scaling_layout.addWidget(QLabel("Scale Factor per Step:")) | |
scaling_layout.addWidget(self.scale_factor) | |
corruption_layout.addWidget(scaling_widget) | |
# Buttons | |
buttons_widget = QWidget() | |
buttons_layout = QHBoxLayout(buttons_widget) | |
self.generate_btn = QPushButton("Generate ASCII") | |
self.corrupt_btn = QPushButton("Corrupt") | |
self.save_btn = QPushButton("Save Result") | |
self.generate_btn.clicked.connect(self.update_ascii) | |
self.corrupt_btn.clicked.connect(self.corrupt_ascii) | |
self.save_btn.clicked.connect(self.save_result) | |
buttons_layout.addWidget(self.generate_btn) | |
buttons_layout.addWidget(self.corrupt_btn) | |
buttons_layout.addWidget(self.save_btn) | |
# Add a new button for batch import | |
self.batch_import_btn = QPushButton("Batch Import") | |
self.batch_import_btn.clicked.connect(self.batch_import_images) | |
buttons_layout.addWidget(self.batch_import_btn) | |
# Add all controls to middle panel | |
middle_layout.addWidget(image_controls) | |
middle_layout.addWidget(corruption_controls) | |
middle_layout.addWidget(buttons_widget) | |
middle_layout.addStretch() | |
# Right panel - Corruption preview and graph | |
right_panel = QWidget() | |
right_layout = QVBoxLayout(right_panel) | |
self.corruption_preview = QTextEdit() | |
self.corruption_preview.setFont(QFont("Courier", 8)) | |
self.plot_widget = pg.PlotWidget() | |
self.plot_widget.setBackground('w') | |
self.plot_widget.showGrid(x=True, y=True) | |
right_layout.addWidget(self.corruption_preview) | |
right_layout.addWidget(self.plot_widget) | |
# Add panels to main layout | |
layout.addWidget(left_panel) | |
layout.addWidget(middle_panel) | |
layout.addWidget(right_panel) | |
# Store corrupted versions | |
self.corrupted_versions = [] | |
def load_image(self, event): | |
file_name, _ = QFileDialog.getOpenFileName(self, "Select Image", "", | |
"Image Files (*.png *.jpg *.jpeg *.bmp)") | |
if file_name: | |
self.current_image = Image.open(file_name) | |
# Display preview | |
pixmap = QPixmap(file_name) | |
scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) | |
self.image_label.setPixmap(scaled_pixmap) | |
self.update_ascii() | |
def image_to_ascii(self, image, width_override=None): | |
# Convert image to grayscale | |
image = image.convert('L') | |
# Apply image adjustments based on sliders | |
brightness = ImageEnhance.Brightness(image).enhance(self.sliders['Brightness'].value() / 100) | |
contrast = ImageEnhance.Contrast(brightness).enhance(self.sliders['Contrast'].value() / 100) | |
# Resize image with minimum size protection | |
width = max(2, width_override if width_override is not None else self.sliders['Max Characters'].value()) | |
aspect_ratio = image.size[1] / image.size[0] | |
height = max(2, int(width * aspect_ratio * 0.5)) # Multiply by 0.5 to account for character aspect ratio | |
image = contrast.resize((width, height)) | |
# Convert pixels to ASCII | |
pixels = np.array(image) | |
ascii_str = "" | |
char_length = len(self.ASCII_CHARS) - 1 | |
for row in pixels: | |
for pixel_val in row: | |
char_index = int((pixel_val / 255.0) * char_length) | |
ascii_str += self.ASCII_CHARS[char_index] | |
ascii_str += "\n" | |
return ascii_str | |
def update_ascii(self): | |
if self.current_image: | |
self.current_ascii = self.image_to_ascii(self.current_image) | |
self.ascii_preview.setText(self.current_ascii) | |
def generate_pattern(self, length, noise_level): | |
x = np.linspace(0, 1, length) | |
pattern_type = self.pattern_type.currentText() | |
max_val = noise_level | |
if pattern_type == 'Constant': | |
pattern = np.full(length, max_val) | |
elif pattern_type == 'Gaussian': | |
mean = random.uniform(0.3, 0.7) | |
std = random.uniform(0.1, 0.3) | |
pattern = max_val * np.exp(-((x - mean) ** 2) / (2 * std ** 2)) | |
else: # Linear Descent | |
pattern = max_val * (1 - x) | |
return pattern | |
def corrupt_ascii(self): | |
if not self.current_ascii and not self.batch_ascii: | |
return | |
steps = self.num_steps.value() | |
self.corrupted_versions = [] | |
images_to_process = self.batch_images if self.batch_ascii else [self.current_image] | |
for image in images_to_process: | |
image_corrupted_versions = [] | |
max_width = self.sliders['Max Characters'].value() | |
# Generate corrupted versions for this image | |
for step in range(steps): | |
noise_level = 100 * (steps - step - 1) / (steps - 1) | |
# Calculate scaled width if scaling is enabled | |
if self.enable_scaling.isChecked(): | |
progress = step / (steps - 1) # Goes from 0 to 1 | |
scale_factor = self.scale_factor.value() / 100.0 | |
# Apply different growth curves | |
curve_type = self.curve_type.currentText() | |
if curve_type == 'Linear': | |
scaled_progress = progress | |
elif curve_type == 'Exponential': | |
scaled_progress = np.exp(3 * progress - 3) # Scaled to [0,1] | |
elif curve_type == 'Logarithmic': | |
scaled_progress = np.log(1 + 9 * progress) / np.log(10) # log10(1 + 9x) | |
elif curve_type == 'Sigmoid': | |
scaled_progress = 1 / (1 + np.exp(-10 * (progress - 0.5))) # Centered sigmoid | |
else: # Square Root | |
scaled_progress = np.sqrt(progress) | |
# Scale factor now modifies the intensity of the chosen curve | |
final_progress = scaled_progress ** scale_factor | |
current_width = int(max_width * final_progress) | |
else: | |
current_width = max_width | |
# Generate ASCII art at current scale | |
current_ascii = self.image_to_ascii(image, current_width) | |
# Apply corruption pattern | |
pattern = self.generate_pattern(len(current_ascii), noise_level) | |
# Apply corruption | |
result = "" | |
result += "```\n" # Add separator before each step | |
for i, char in enumerate(current_ascii): | |
if char == '\n': | |
result += char | |
continue | |
if random.random() * 100 < pattern[i]: | |
result += random.choice(self.ASCII_CHARS) | |
else: | |
result += char | |
result += "\n```\n" # Add separator after each step | |
image_corrupted_versions.append(result) | |
self.corrupted_versions.append(image_corrupted_versions) | |
# Update corruption preview with the last processed image | |
full_output = "" | |
for i, version in enumerate(self.corrupted_versions[-1]): | |
full_output += f"Step {i + 1}:\n{version}\n\n" | |
self.corruption_preview.setText(full_output) | |
def batch_import_images(self): | |
file_names, _ = QFileDialog.getOpenFileNames(self, "Select Images", "", | |
"Image Files (*.png *.jpg *.jpeg *.bmp)") | |
if file_names: | |
self.batch_images = [Image.open(file_name) for file_name in file_names] | |
self.batch_ascii = [] | |
for image in self.batch_images: | |
ascii_art = self.image_to_ascii(image) | |
self.batch_ascii.append(ascii_art) | |
# Update preview with the first image | |
self.current_image = self.batch_images[0] | |
pixmap = QPixmap(file_names[0]) | |
scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) | |
self.image_label.setPixmap(scaled_pixmap) # Fixed typo here | |
self.update_ascii() | |
def save_result(self): | |
if not self.corrupted_versions: | |
return | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
filename, _ = QFileDialog.getSaveFileName(self, "Save Result", | |
f"ascii_art_{timestamp}.txt", | |
"Text Files (*.txt)") | |
if filename: | |
with open(filename, 'w', encoding='utf-8') as f: | |
for img_index, image_versions in enumerate(self.corrupted_versions, 1): | |
f.write(f"# IMAGE {img_index}\n\n") | |
for step, version in enumerate(image_versions, 1): | |
f.write(f"Step {step}:\n{version}\n\n") | |
f.write("=" * 50 + "\n\n") | |
if __name__ == '__main__': | |
app = QApplication(sys.argv) | |
window = ASCIICorruptor() | |
window.show() | |
sys.exit(app.exec_()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment