效果展示:
目录结构:
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_data
app = 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 # 最大文件大小为5MB
def 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 args
def 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_EXTENSIONS
def 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.shape
cell_width = width / opt.num_cols
cell_height = scale * cell_width
num_rows = int(height / cell_height)
# 如果列数或行数过多,使用默认设置
if num_cols > width or num_rows > height:
print("列数或行数过多,使用默认设置。")
cell_width = 6
cell_height = 12
num_cols = int(width / cell_width)
num_rows = int(height / cell_height)
# 获取字符的宽度和高度
char_width, char_height = font.getsize(sample_character)
out_width = char_width * num_cols
out_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 = filename
opt.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, ImageOps
def 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_height
out_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 = 0
incremental_step = (zipped_lists[-1][0] - zipped_lists[0][0]) / num_chars
current_value = zipped_lists[0][0]
for value, char in zipped_lists:
if value >= current_value:
result += char
counter += 1
current_value += incremental_step
if counter == num_chars:
break
if result[-1] != zipped_lists[-1][1]:
result += zipped_lists[-1][1]
return result
def get_data(language, mode):
if language == "general":
from alphabets import GENERAL as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "english":
from alphabets import ENGLISH as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "german":
from alphabets import GERMAN as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "french":
from alphabets import FRENCH as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "italian":
from alphabets import ITALIAN as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "polish":
from alphabets import POLISH as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "portuguese":
from alphabets import PORTUGUESE as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "spanish":
from alphabets import SPANISH as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "A"
scale = 2
elif language == "russian":
from alphabets import RUSSIAN as character
font = ImageFont.truetype("fonts/DejaVuSansMono-Bold.ttf", size=20)
sample_character = "Ш"
scale = 2
elif language == "chinese":
from alphabets import CHINESE as character
font = ImageFont.truetype("fonts/simsun.ttc", size=10)
sample_character = "制"
scale = 1
elif language == "korean":
from alphabets import KOREAN as character
font = ImageFont.truetype("fonts/arial-unicode.ttf", size=10)
sample_character = "ㅊ"
scale = 1
elif language == "japanese":
from alphabets import JAPANESE as character
font = ImageFont.truetype("fonts/arial-unicode.ttf", size=10)
sample_character = "お"
scale = 1
else:
print("Invalid language")
return None, None, None, None
try:
if len(character) > 1:
char_list = character[mode]
else:
char_list = character["standard"]
except:
print("Invalid mode for {}".format(language))
return None, None, None, None
if 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)