Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ascii_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"width": 100,
"height": null,
"character_set": "standard",
"preserve_color": false,
"invert_chars": false,
"brightness": 1.0,
"contrast": 1.0,
"frame_rate": 10,
"output_format": "terminal"
}
341 changes: 341 additions & 0 deletions asciify_advanced.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
import cv2
import numpy as np
import time
import json
import argparse
import threading
from PIL import Image
from typing import List, Tuple, Optional, Dict, Any
from dataclasses import dataclass
from pathlib import Path

@dataclass
class ASCIIConfig:
"""Configuration class for ASCII art generation"""
width: int = 100
height: Optional[int] = None
character_set: str = "standard"
preserve_color: bool = False
invert_chars: bool = False
brightness: float = 1.0
contrast: float = 1.0
frame_rate: int = 10
output_format: str = "terminal"

class ASCIIArtEngine:
"""Advanced ASCII Art generation engine with video support"""

CHARACTER_SETS = {
'standard': ['.',',',':',';','+','*','?','%','S','#','@'],
'dense': [' ','░','▒','▓','█'],
'minimal': [' ','.','o','#'],
'binary': [' ','█'],
'blocks': [' ','▁','▂','▃','▄','▅','▆','▇','█'],
'braille': [' ','⠁','⠃','⠇','⠏','⠟','⠿','⡿','⣿'],
'custom': []
}

ANSI_COLORS = {
'reset': '\033[0m',
'red': '\033[91m',
'green': '\033[92m',
'yellow': '\033[93m',
'blue': '\033[94m',
'magenta': '\033[95m',
'cyan': '\033[96m',
'white': '\033[97m'
}

def __init__(self, config: ASCIIConfig):
self.config = config
self.chars = self._get_character_set()
self.frame_buffer = []
self.is_streaming = False

def _get_character_set(self) -> List[str]:
"""Get the character set based on configuration"""
chars = self.CHARACTER_SETS.get(self.config.character_set, self.CHARACTER_SETS['standard'])
return chars[::-1] if self.config.invert_chars else chars

def _adjust_image(self, image: np.ndarray) -> np.ndarray:
"""Apply brightness and contrast adjustments"""
if self.config.brightness != 1.0 or self.config.contrast != 1.0:
image = cv2.convertScaleAbs(image, alpha=self.config.contrast, beta=self.config.brightness * 50)
return image

def _resize_image(self, image: np.ndarray) -> np.ndarray:
"""Resize image maintaining aspect ratio"""
h, w = image.shape[:2] if len(image.shape) == 3 else image.shape

new_width = self.config.width
if self.config.height:
new_height = self.config.height
else:
aspect_ratio = h / w
new_height = int(aspect_ratio * new_width * 0.5) # 0.5 for character aspect ratio

return cv2.resize(image, (new_width, new_height))

def _image_to_ascii(self, image: np.ndarray, preserve_color: bool = False) -> str:
"""Convert image to ASCII art"""
if len(image.shape) == 3:
if preserve_color:
return self._color_ascii_conversion(image)
else:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image

gray = self._adjust_image(gray)
gray = self._resize_image(gray)

# Map pixel values to ASCII characters
bucket_size = 256 // len(self.chars)
ascii_image = []

for row in gray:
ascii_row = ''.join([self.chars[min(pixel // bucket_size, len(self.chars) - 1)] for pixel in row])
ascii_image.append(ascii_row)

return '\n'.join(ascii_image)

def _color_ascii_conversion(self, image: np.ndarray) -> str:
"""Convert image to colored ASCII art using ANSI color codes"""
image = self._resize_image(image)
h, w = image.shape[:2]

ascii_image = []
for y in range(h):
row = ""
for x in range(w):
b, g, r = image[y, x]
gray_val = int(0.299 * r + 0.587 * g + 0.114 * b)

# Choose character based on intensity
bucket_size = 256 // len(self.chars)
char = self.chars[min(gray_val // bucket_size, len(self.chars) - 1)]

# Choose closest ANSI color
color_code = self._get_closest_ansi_color(r, g, b)
row += f"{color_code}{char}{self.ANSI_COLORS['reset']}"

ascii_image.append(row)

return '\n'.join(ascii_image)

def _get_closest_ansi_color(self, r: int, g: int, b: int) -> str:
"""Get the closest ANSI color code for RGB values"""
color_map = {
(255, 0, 0): 'red',
(0, 255, 0): 'green',
(255, 255, 0): 'yellow',
(0, 0, 255): 'blue',
(255, 0, 255): 'magenta',
(0, 255, 255): 'cyan',
(255, 255, 255): 'white'
}

min_distance = float('inf')
closest_color = 'white'

for color_rgb, color_name in color_map.items():
distance = sum((a - b) ** 2 for a, b in zip((r, g, b), color_rgb))
if distance < min_distance:
min_distance = distance
closest_color = color_name

return self.ANSI_COLORS[closest_color]

def process_image(self, image_path: str) -> str:
"""Process a single image to ASCII art"""
image = cv2.imread(image_path)
if image is None:
raise FileNotFoundError(f"Unable to load image: {image_path}")

return self._image_to_ascii(image, self.config.preserve_color)

def process_video(self, video_path: str, output_path: Optional[str] = None) -> None:
"""Process video file to animated ASCII art"""
cap = cv2.VideoCapture(video_path)

if not cap.isOpened():
raise FileNotFoundError(f"Unable to open video: {video_path}")

fps = cap.get(cv2.CAP_PROP_FPS)
frame_delay = 1.0 / min(fps, self.config.frame_rate)

frames = []
frame_count = 0

print(f"Processing video frames...")

while True:
ret, frame = cap.read()
if not ret:
break

ascii_frame = self._image_to_ascii(frame, self.config.preserve_color)
frames.append(ascii_frame)
frame_count += 1

if frame_count % 10 == 0:
print(f"Processed {frame_count} frames...")

cap.release()

if output_path:
self._save_animation(frames, output_path, frame_delay)
else:
self._play_animation(frames, frame_delay)

def _play_animation(self, frames: List[str], frame_delay: float) -> None:
"""Play ASCII animation in terminal"""
print("\nPlaying ASCII animation (Press Ctrl+C to stop)...")
time.sleep(2)

try:
while True:
for frame in frames:
print("\033[2J\033[H", end="") # Clear screen and move cursor to top
print(frame)
time.sleep(frame_delay)
except KeyboardInterrupt:
print("\nAnimation stopped.")

def _save_animation(self, frames: List[str], output_path: str, frame_delay: float) -> None:
"""Save animation frames to file"""
with open(output_path, 'w') as f:
f.write(f"# ASCII Animation - {len(frames)} frames, {frame_delay:.3f}s delay\n\n")
for i, frame in enumerate(frames):
f.write(f"# Frame {i + 1}\n")
f.write(frame)
f.write(f"\n{'='*50}\n\n")

print(f"Animation saved to: {output_path}")

def start_webcam_stream(self) -> None:
"""Start real-time webcam ASCII art streaming"""
cap = cv2.VideoCapture(0)

if not cap.isOpened():
raise RuntimeError("Unable to access webcam")

self.is_streaming = True
frame_delay = 1.0 / self.config.frame_rate

print("Starting webcam ASCII stream (Press 'q' to quit)...")

try:
while self.is_streaming:
ret, frame = cap.read()
if not ret:
break

ascii_frame = self._image_to_ascii(frame, self.config.preserve_color)

print("\033[2J\033[H", end="") # Clear screen
print(ascii_frame)
print(f"\nWebcam ASCII Stream - Press 'q' to quit")

time.sleep(frame_delay)

except KeyboardInterrupt:
pass
finally:
cap.release()
self.is_streaming = False
print("\nWebcam stream stopped.")

class ConfigManager:
"""Manage configuration settings"""

@staticmethod
def load_config(config_path: str) -> ASCIIConfig:
"""Load configuration from JSON file"""
try:
with open(config_path, 'r') as f:
data = json.load(f)
return ASCIIConfig(**data)
except FileNotFoundError:
print(f"Config file not found: {config_path}. Using defaults.")
return ASCIIConfig()
except json.JSONDecodeError as e:
print(f"Invalid JSON in config file: {e}. Using defaults.")
return ASCIIConfig()

@staticmethod
def save_config(config: ASCIIConfig, config_path: str) -> None:
"""Save configuration to JSON file"""
with open(config_path, 'w') as f:
json.dump(config.__dict__, f, indent=2)
print(f"Configuration saved to: {config_path}")

@staticmethod
def create_default_config(config_path: str) -> None:
"""Create a default configuration file"""
default_config = ASCIIConfig()
ConfigManager.save_config(default_config, config_path)

def main():
parser = argparse.ArgumentParser(description="Advanced ASCII Art Generator with Animation Support")
parser.add_argument("input", help="Input image or video file path")
parser.add_argument("--config", "-c", default="ascii_config.json", help="Configuration file path")
parser.add_argument("--width", "-w", type=int, default=100, help="Output width")
parser.add_argument("--height", type=int, help="Output height")
parser.add_argument("--charset", choices=list(ASCIIArtEngine.CHARACTER_SETS.keys()),
default="standard", help="Character set to use")
parser.add_argument("--color", action="store_true", help="Preserve colors using ANSI codes")
parser.add_argument("--invert", action="store_true", help="Invert character intensity")
parser.add_argument("--brightness", type=float, default=1.0, help="Brightness adjustment")
parser.add_argument("--contrast", type=float, default=1.0, help="Contrast adjustment")
parser.add_argument("--fps", type=int, default=10, help="Frame rate for video processing")
parser.add_argument("--output", "-o", help="Output file path")
parser.add_argument("--webcam", action="store_true", help="Start webcam ASCII stream")
parser.add_argument("--create-config", action="store_true", help="Create default config file")

args = parser.parse_args()

if args.create_config:
ConfigManager.create_default_config(args.config)
return

# Load or create configuration
if Path(args.config).exists():
config = ConfigManager.load_config(args.config)
else:
config = ASCIIConfig()

# Override config with command line arguments
config.width = args.width
if args.height:
config.height = args.height
config.character_set = args.charset
config.preserve_color = args.color
config.invert_chars = args.invert
config.brightness = args.brightness
config.contrast = args.contrast
config.frame_rate = args.fps

engine = ASCIIArtEngine(config)

if args.webcam:
engine.start_webcam_stream()
elif args.input:
# Determine if input is image or video
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm'}
input_path = Path(args.input)

if input_path.suffix.lower() in video_extensions:
engine.process_video(args.input, args.output)
else:
ascii_art = engine.process_image(args.input)

if args.output:
with open(args.output, 'w') as f:
f.write(ascii_art)
print(f"ASCII art saved to: {args.output}")
else:
print(ascii_art)

if __name__ == "__main__":
main()
Loading