我用这种方法将一张普通的图片转换成ASCII艺术.但即使是在性能不强的处理器上处理小图像,也只会扼杀它们.有可能对此进行优化吗?我试着用麻木做一些东西,但我什么都没做.任何关于优化的帮助都将不胜感激,谢谢.以下是我失败的try (

算法摘自此处:https://github.com/Akascape/Ascify-Art

我的代码是:

from PIL import Image, ImageDraw, ImageFont
import math

def make_magic_old(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    # Rounding scale
    scaleFactor = round(scale, 3)
    # Calculate the length of the character list
    charLength = len(list(chars))
    # Calculate the interval for converting a pixel value into a character
    interval = charLength / 256
    # Convert the image to RGB
    photo = photo.convert("RGB")
    # Load font
    fnt = ImageFont.truetype("assets/fonts/FiraCode-Bold.ttf", char_size)
    # Get size of the image
    width, height = photo.size
    # Scaling the image
    photo = photo.resize((int(scaleFactor * width), int(scaleFactor * height * (char_width / char_height))), Image.Resampling.NEAREST)
    # Getting the sizes in a new way after scaling
    width, height = photo.size
    # Load pixels
    pix = photo.load()
    # Create a new image to display the result
    outputImage = Image.new("RGB", (char_width * width, char_height * height), color="black")
    # Create a drawing tool
    draw = ImageDraw.Draw(outputImage)
    # Replace pixes to text
    for i in range(height):
        for j in range(width):
            r, g, b = pix[j, i]
            # Calculate the average color value
            h = int(r / 3 + g / 3 + b / 3)
            # Convert pixel colors
            pix[j, i] = (h, h, h)
            # Display a symbol instead of a pixel
            draw.text((j * char_width, i * char_height), chars[math.floor(h * interval)], font=fnt, fill=(r, g, b))

    return outputImage

def main():
    photo = Image.open("test.png")
    result_photo = make_magic(photo)
    result_photo.save("result.jpg")
    print("Done!")
    
if __name__ == "__main__":
    main()

try 用NumPy进行优化:

import numpy as np

def make_magic(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    # Rounding scale
    scaleFactor = round(scale, 3)
    # Calculate the length of the character list
    charLength = len(chars)
    # Convert the image to RGB and then to numpy array
    photo = np.array(photo.convert("RGB"))
    # Load font
    fnt = ImageFont.truetype("assets/fonts/FiraCode-Bold.ttf", char_size)
    # Get size of the image
    height, width, _ = photo.shape
    # Scaling the image
    photo = np.array(Image.fromarray(photo).resize((int(scaleFactor * width), int(scaleFactor * height * (char_width / char_height))), Image.NEAREST))
    # Getting the sizes in a new way after scaling
    height, width, _ = photo.shape
    # Convert the image to grayscale
    grayscale_photo = np.mean(photo, axis=2).astype(np.uint8)
    # Calculate indices for character selection
    indices = (grayscale_photo * (charLength / 256)).astype(int)
    # Create a new image to display the result
    outputImage = Image.new("RGB", (char_width * width, char_height * height), color="black")
    # Create a drawing tool
    draw = ImageDraw.Draw(outputImage)
    # Create character array
    char_array = np.array(list(chars))
    # Replace pixels with text
    for i in range(height):
        for j in range(width):
            draw.text((j * char_width, i * char_height), char_array[indices[i, j]], font=fnt, fill=tuple(photo[i, j]))
    return outputImage

Img before: Img before

Img after: Img after

推荐答案

原来的算法真的没有那么糟糕,所以很多加速将是相当具有挑战性的.对于非常小的图像输出,这里提供的解决方案大约慢2倍,对于非常大的输出,这个解决方案大约快30倍.

在我的机器上测试,即使使用SSD、PIL.Image.openPIL.Image.save,也会对运行时产生非常大的影响,特别是在较小的文件上.这是不可避免的,所以我将重点放在图像创建组件上.

这个解决方案的基本概念是预先生成所有字母,然后将它们平铺成整体图像,利用numpynumba‘S惊人的协同作用进行基本的矩阵运算.

字体步骤并不是特别适合numba编译,所以就留在了python-land中.如果您需要处理许多文件,您可以在循环中调用Sub函数,省go 重复的字体步骤.

以下是我提出的解决方案,其中包含一些额外的测试样板代码:

import time

from PIL import Image, ImageDraw, ImageFont
import math
import numba
import numpy as np

def make_magic_old(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    # Rounding scale
    scaleFactor = round(scale, 3)
    # Calculate the length of the character list
    charLength = len(list(chars))
    # Calculate the interval for converting a pixel value into a character
    interval = charLength / 256
    # Convert the image to RGB
    photo = photo.convert("RGB")
    # Load font
    fnt = ImageFont.truetype("font.ttf", char_size)
    # Get size of the image
    width, height = photo.size
    # Scaling the image
    photo = photo.resize((int(scaleFactor * width), int(scaleFactor * height * (char_width / char_height))),
                         Image.Resampling.NEAREST)
    # Getting the sizes in a new way after scaling
    width, height = photo.size
    # Load pixels
    pix = photo.load()
    # Create a new image to display the result
    outputImage = Image.new("RGB", (char_width * width, char_height * height), color="black")
    # Create a drawing tool
    draw = ImageDraw.Draw(outputImage)
    # Replace pixes to text
    for i in range(height):
        for j in range(width):
            r, g, b = pix[j, i]
            # Calculate the average color value
            h = int(r / 3 + g / 3 + b / 3)
            # Convert pixel colors
            pix[j, i] = (h, h, h)
            # Display a symbol instead of a pixel
            draw.text((j * char_width, i * char_height), chars[math.floor(h * interval)], font=fnt, fill=(r, g, b))

    return outputImage


def make_magic(photo, chars="01", char_size=15, char_width=10, char_height=18, scale=0.09):
    # Convert the image to RGB
    photo = photo.convert("RGB")
    # Load font
    fnt = ImageFont.truetype("font.ttf", char_size)
    # Make character masks to tile into output image
    char_masks = np.empty((len(chars), char_height, char_width, 3), np.ubyte)
    for i, char in enumerate(chars):
        tim = Image.new('RGB', (char_width, char_height), color='black')
        draw = ImageDraw.Draw(tim)
        draw.text((0, 0), char, font=fnt, fill=(255, 255, 255))
        char_masks[i, :] = np.array(tim)
    # Call the numpy + numba optimized function
    new_img_array = _make_magic_sub(np.array(photo), char_masks, char_width, char_height, scale)
    return Image.fromarray(new_img_array, 'RGB')


@numba.njit(cache=True, parallel=True)
def _make_magic_sub(photo, char_masks, char_width, char_height, scale):
    interval = 1 / char_masks.shape[0]
    new_size = (int(photo.shape[0] * scale * char_width / char_height), int(photo.shape[1] * scale), 3)
    outimage = np.empty((new_size[0] * char_height, new_size[1] * char_width, 3), np.ubyte)

    for i in numba.prange(new_size[0]):
        for j in range(new_size[1]):
            rgb = photo[int(i / new_size[0] * photo.shape[0]), int(j / new_size[1] * photo.shape[1])] / 255
            char_num = int(np.floor(np.sum(rgb) / 3 / interval))
            outimage[i * char_height: (i + 1) * char_height, j * char_width: (j + 1) * char_width, :] = char_masks[char_num] * rgb

    return outimage

def _gt(s=0.0):
    return time.perf_counter() - s

def main():
    photo = Image.open("test.png")
    N = 10
    for scale in [0.01, 0.05, 0.1, 0.2, 0.5, 1.0]:
        for fun in [make_magic_old, make_magic]:
            fun(photo)  # To skip any caching / compilation times
            s = _gt()
            for i in range(N):
                result_photo = fun(photo, scale=scale)
            e = _gt(s)
            print(f'{fun.__name__:16}{scale:4.2f}  : {e / N * 1000:7.1f} ms')
        print()

    res_old = make_magic_old(photo, scale=0.2)
    res_new = make_magic(photo, scale=0.2)
    res_old.save('result_old.png')
    res_new.save('result_new.png')

if __name__ == "__main__":
    main()

它提供了以下结果:

make_magic_old  0.01  :     2.2 ms
make_magic      0.01  :     4.7 ms

make_magic_old  0.05  :    18.6 ms
make_magic      0.05  :     5.1 ms

make_magic_old  0.10  :    65.9 ms
make_magic      0.10  :     6.9 ms

make_magic_old  0.20  :   256.1 ms
make_magic      0.20  :    13.1 ms

make_magic_old  0.50  :  1601.3 ms
make_magic      0.50  :    58.7 ms

make_magic_old  1.00  :  6379.3 ms
make_magic      1.00  :   194.2 ms

在Windows 10、i9-10900K、Python 3.11.4上测试

你的结果可能会有很大的不同,我运行的处理器远不是"强大的处理器",但我认为这将帮助你在大多数多线程处理器上,你可以看到,我们在最大的输出图像上获得了大约32倍的速度.

Output from the new code: new output

And output from the old code for comparison: old output

如果你有什么问题就告诉我.

Python相关问答推荐

Pythind 11无法弄清楚如何访问tuple元素

Python会扔掉未使用的表情吗?

如何在msgraph.GraphServiceClient上进行身份验证?

韦尔福德方差与Numpy方差不同

抓取rotowire MLB球员新闻并使用Python形成表格

如何根据参数推断对象的返回类型?

将两只Pandas rame乘以指数

在Python Attrs包中,如何在field_Transformer函数中添加字段?

在Python中管理打开对话框

在vscode上使用Python虚拟环境时((env))

在含噪声的3D点网格中识别4连通点模式

多指标不同顺序串联大Pandas 模型

用渐近模计算含符号的矩阵乘法

使用特定值作为引用替换数据框行上的值

如何获取Python synsets列表的第一个内容?

为什么在FastAPI中创建与数据库的连接时需要使用生成器?

如何使用matplotlib查看并列直方图

当输入是字典时,`pandas. concat`如何工作?

大型稀疏CSR二进制矩阵乘法结果中的错误

对于数组中的所有元素,Pandas SELECT行都具有值