效果展示:
目录结构:
alphabets.py
GENERAL = {"simple": "@%#*+=-:. ","complex": "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
}
# Full list could be found here https://github.com/tsroten/zhon/tree/develop/zhon/cedict
CHINESE = {"standard": "龘䶑瀰幗獼鑭躙䵹觿䲔釅欄鐮䥯鶒獭鰽襽螻鰱蹦屭繩圇婹歜剛屧磕媿慪像僭堳噞呱棒偁呣塙唑浠唼刻凌咄亟拮俗参坒估这聿布允仫忖玗甴木亪女去凸五圹亐囗弌九人亏产斗丩艹刂彳丬了5丄三亻讠厂丆丨1二宀冖乛一丶、",
}KOREAN = {"standard": "ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㅏㅑㅓㅕㅗㅛㅜㅠㅡㅣ"
}JAPANESE = {"hiragana": "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん","katakana": "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
}ENGLISH = {"standard": "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
}RUSSIAN = {"standard": "АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя"
}GERMAN = {"standard": "AaÄäBbßCcDdEeFfGgHhIiJjKkLlMmNnOoÖöPpQqRrSsTtUuÜüVvWwXxYyZz"
}FRENCH = {"standard": "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZzÆ挜ÇçÀàÂâÉéÈèÊêËëÎîÎïÔôÛûÙùŸÿ"
}SPANISH = {"standard": "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZzÑñáéíóú¡¿"
}ITALIAN = {"standard": "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZzÀÈàèéìòù"
}PORTUGUESE = {"standard": "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZzàÀáÁâÂãÃçÇéÉêÊíÍóÓôÔõÕúÚ"
}POLISH = {"standard": "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpRrSsTtUuWwYyZzĄąĘęÓóŁłŃńŻżŚśĆ揟"
}
app.py
import os
import hashlib
import argparse
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageOps
from flask import Flask, request, send_file, render_template_string
from utils import get_dataapp = Flask(__name__)# 确保上传和输出目录存在
if not os.path.exists("uploads"):os.makedirs("uploads")
if not os.path.exists("outputs"):os.makedirs("outputs")# 文件处理常量
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp'}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 最大文件大小为5MBdef get_args():"""获取命令行参数"""parser = argparse.ArgumentParser("Image to ASCII")parser.add_argument("--input", type=str, default="uploads/input.jpg", help="输入图片路径")parser.add_argument("--output", type=str, default="outputs/output.jpg", help="输出图片路径")parser.add_argument("--language", type=str, default="english") # 使用的字符语言parser.add_argument("--mode", type=str, default="standard") # ASCII 模式parser.add_argument("--background", type=str, default="black", choices=["black", "white"],help="背景颜色")parser.add_argument("--num_cols", type=int, default=300, help="输出图片的宽度字符数")parser.add_argument("--scale", type=int, default=2, help="输出图片的缩放比例")args = parser.parse_args()return argsdef md5_filename(filename):"""返回文件名的MD5哈希值,用于生成唯一的文件名"""hash_md5 = hashlib.md5()hash_md5.update(filename.encode('utf-8')) # 更新哈希值return hash_md5.hexdigest()def allowed_file(filename):"""检查文件是否具有允许的扩展名"""return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONSdef convert_image_to_ascii(opt):"""将图像转换为ASCII字符图像:param opt: 命令行参数对象,包含转换的配置"""# 根据背景颜色设置背景if opt.background == "white":bg_code = (255, 255, 255)else:bg_code = (0, 0, 0)# 获取字符列表、字体和缩放参数char_list, font, sample_character, scale = get_data(opt.language, opt.mode)num_chars = len(char_list)num_cols = opt.num_cols# 检查输入文件是否存在if not os.path.exists(opt.input):print(f"错误:文件 {opt.input} 不存在!")return# 读取图像并验证image = cv2.imread(opt.input, cv2.IMREAD_COLOR)if image is None:print(f"错误:无法加载图片 {opt.input}")return# 将图像从BGR格式转换为RGB格式image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)# 获取图像尺寸height, width, _ = image.shapecell_width = width / opt.num_colscell_height = scale * cell_widthnum_rows = int(height / cell_height)# 如果列数或行数过多,使用默认设置if num_cols > width or num_rows > height:print("列数或行数过多,使用默认设置。")cell_width = 6cell_height = 12num_cols = int(width / cell_width)num_rows = int(height / cell_height)# 获取字符的宽度和高度char_width, char_height = font.getsize(sample_character)out_width = char_width * num_colsout_height = scale * char_height * num_rows# 创建输出图像out_image = Image.new("RGB", (out_width, out_height), bg_code)draw = ImageDraw.Draw(out_image)# 逐个处理图像区域,转换为字符for i in range(num_rows):for j in range(num_cols):partial_image = image[int(i * cell_height):min(int((i + 1) * cell_height), height),int(j * cell_width):min(int((j + 1) * cell_width), width), :]partial_avg_color = np.sum(np.sum(partial_image, axis=0), axis=0) / (cell_height * cell_width)partial_avg_color = tuple(partial_avg_color.astype(np.int32).tolist())# 根据平均色值选择合适的字符char = char_list[min(int(np.mean(partial_image) * num_chars / 255), num_chars - 1)]draw.text((j * char_width, i * char_height), char, fill=partial_avg_color, font=font)# 根据背景颜色裁剪图像if opt.background == "white":cropped_image = ImageOps.invert(out_image).getbbox()else:cropped_image = out_image.getbbox()out_image = out_image.crop(cropped_image)out_image.save(opt.output)@app.route('/')
def index():"""渲染首页HTML,用户可以上传图片"""return render_template_string("""<html><head><style>body {font-family: Arial, sans-serif;background-color: #f4f4f9;margin: 0;padding: 0;display: flex;justify-content: center;align-items: center;height: 100vh;}.container {background-color: #fff;padding: 30px;border-radius: 8px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);width: 300px;text-align: center;}h1 {font-size: 24px;color: #333;}form {margin-top: 20px;}input[type="file"] {display: block;margin: 10px auto;padding: 10px;border: 1px solid #ccc;border-radius: 4px;width: 100%;}input[type="submit"] {background-color: #4CAF50;color: white;border: none;padding: 12px 20px;border-radius: 4px;cursor: pointer;width: 100%;font-size: 16px;}input[type="submit"]:hover {background-color: #45a049;}.footer {margin-top: 20px;font-size: 14px;color: #777;}</style></head><body><div class="container"><h1>彩色图片转化器YFREE</h1><form action="/upload" method="POST" enctype="multipart/form-data"><input type="file" name="image" required><input type="submit" value="上传图片"></form><div class="footer"><p>支持开源 <a href="https://github.com/vietnh1009/ASCII-generator" target="_blank">github</a></p></div></div></body></html>""")@app.route('/upload', methods=['POST'])
def upload():"""处理上传的图片,将其转换为ASCII图像并返回"""if 'image' not in request.files:return '没有文件上传'file = request.files['image']if file.filename == '':return '没有选择文件'# 检查文件扩展名if not allowed_file(file.filename):return '无效的文件类型,请上传图片。'# 检查文件大小file.seek(0, os.SEEK_END)file_size = file.tell()if file_size > MAX_FILE_SIZE:return '文件太大,最大支持文件大小为5MB。'file.seek(0) # 重置文件指针# 保存文件md5_filename_value = md5_filename(file.filename)filename = os.path.join("uploads", md5_filename_value + os.path.splitext(file.filename)[1])file.save(filename)if not os.path.exists(filename):return '文件上传失败。'# 获取参数并处理图像opt = get_args()opt.input = filenameopt.output = os.path.join("outputs", f"output_{md5_filename_value}.jpg")# 调用转换函数convert_image_to_ascii(opt)return send_file(opt.output, as_attachment=True)if __name__ == '__main__':app.run(port=8979)
Dockerfile 和 requestments.txt
# 使用python的官方镜像作为基础镜像
FROM python:3.6-slim# 设置工作目录
WORKDIR /app# 复制requirements.txt到容器中
COPY requirements.txt .# 安装python依赖
RUN pip install --no-cache-dir -r requirements.txt# 复制项目的所有文件到容器
COPY . .# 暴露端口
EXPOSE 8979# 指定启动命令
CMD ["python", "app.py"]click==8.0.4
colorama==0.4.5
dataclasses==0.8
Flask==2.0.3
importlib-metadata==4.8.3
itsdangerous==2.0.1
Jinja2==3.0.3
MarkupSafe==2.0.1
numpy==1.19.5
opencv-contrib-python==4.6.0.66
Pillow==8.4.0
typing_extensions==4.1.1
Werkzeug==2.0.3
zipp==3.6.0
opencv-python-headless
utils.py
import numpy as np
from PIL import Image, ImageFont, ImageDraw, ImageOpsdef sort_chars(char_list, font, language):if language == "chinese":char_width, char_height = font.getsize("制")elif language == "korean":char_width, char_height = font.getsize("ㅊ")elif language == "japanese":char_width, char_height = font.getsize("あ")elif language in ["english", "german", "french", "spanish", "italian", "portuguese", "polish"]:char_width, char_height = font.getsize("A")elif language == "russian":char_width, char_height = font.getsize("A")num_chars = min(len(char_list), 100)out_width = char_width * len(char_list)out_height = char_heightout_image = Image.new("L", (out_width, out_height), 255)draw = ImageDraw.Draw(out_image)draw.text((0, 0), char_list, fill=0, font=font)cropped_image = ImageOps.invert(out_image).getbbox()out_image = out_image.crop(cropped_image)brightness = [np.mean(np.array(out_image)[:, 10 * i:10 * (i + 1)]) for i in range(len(char_list))]char_list = list(char_list)zipped_lists = zip(brightness, char_list)zipped_lists = sorted(zipped_lists)result = ""counter = 0incremental_step = (zipped_lists[-1][0] - zipped_lists[0][0]) / num_charscurrent_value = zipped_lists[0][0]for value, char in zipped_lists:if value >= current_value:result += charcounter += 1current_value += incremental_stepif counter == num_chars:breakif result[-1] != zipped_lists[-1][1]:result += zipped_lists[-1][1]return resultdef get_data(language, mode):if language == "general":from alphabets import GENERAL as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "english":from alphabets import ENGLISH as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "german":from alphabets import GERMAN as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "french":from alphabets import FRENCH as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "italian":from alphabets import ITALIAN as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "polish":from alphabets import POLISH as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "portuguese":from alphabets import PORTUGUESE as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "spanish":from alphabets import SPANISH as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "A"scale = 2elif language == "russian":from alphabets import RUSSIAN as characterfont = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)sample_character = "Ш"scale = 2elif language == "chinese":from alphabets import CHINESE as characterfont = ImageFont.truetype("fonts/simsun.ttc", size=10)sample_character = "制"scale = 1elif language == "korean":from alphabets import KOREAN as characterfont = ImageFont.truetype("fonts/arial-unicode.ttf", size=10)sample_character = "ㅊ"scale = 1elif language == "japanese":from alphabets import JAPANESE as characterfont = ImageFont.truetype("fonts/arial-unicode.ttf", size=10)sample_character = "お"scale = 1else:print("Invalid language")return None, None, None, Nonetry:if len(character) > 1:char_list = character[mode]else:char_list = character["standard"]except:print("Invalid mode for {}".format(language))return None, None, None, Noneif language != "general":char_list = sort_chars(char_list, font, language)return char_list, font, sample_character, scale
支持开源:
vietnh1009/ASCII-generator: ASCII generator (image to text, image to image, video to video) (github.com)