我如何用Python创造了一种10000个DALL-E积分都无法购买的生成艺术

用Python创造无价的生成艺术

Python 和 Pillow:如何编写 DALL-E 无法完成的代码

在这篇博客文章中,我将展示使用 Python 编程语言以及 Pillow 和 Torch 库创建的一些生成艺术作品。我的艺术灵感来自奥地利音乐作曲家和视觉艺术家 Roman Haubenstock-Ramati 的视觉构图。

在 2021 年初,我经常浏览 Catawiki,因为我想购买一些艺术品来装饰我的家庭办公室。当我在 2021 年初在 Catawiki 上看到 Haubenstock-Ramati 的作品时,我立刻被他的参数艺术的复杂和美丽所吸引。我一直想用我的编码技能做一些创意项目,受到了开发能够产生类似输出的代码的启发。下面的图片是 Haubenstock-Ramati 创作的一张图片,它是我受到启发的例子。

Konstellationen, 1970/1971 by Roman Haubenstock-Ramati

在 2022 年 4 月 Dall-E 2 发布之后,我尝试使用该模型生成类似于 Haubenstock-Ramati 的作品的艺术品。要求模型这样做是一个有争议的话题,因为有关 AI 模型能否产生与艺术家作品如此相似的输出而被视为对原作的版权侵犯的合理担忧。这个讨论超出了本博文的范围,但我想明确表示,我输入 Dall-E 的提示并不打算产生 Haubenstock-Ramati 作品的精确副本,也不打算贬低他的作品。同样,我编写的代码也不打算分发他的作品的副本,而只是演示如何使用 Python 创建视觉几何构图。

DALL-E 的输出很有趣,但它并没有完全捕捉到他原作的本质。输出缺乏 Haubenstock-Ramati 艺术中精确的约束和复杂性。我尝试了许多提示的变化,但始终无法接近我想要的效果。

Some of the outputs generated by Dall-E given my prompt: “Create a painting composition in the style of Roman Haubenstock-Ramati that incorporates elements of graphic notation and experimental musical composition. The painting should be predominantly black and white with bold lines and geometric shapes, and should include a central motif that represents the theme of the piece.”

为了简化过程,我向 Dall-E 提出了一个更简单的请求:“画一条连接到一个矩形的垂直线,将一个正方形连接到该线,将该正方形与另一条垂直线连接到另一个矩形,最后用另一条垂直线将矩形连接到一个圆形。”出人意料的是,结果出乎意料。尽管提示很简单,但 Dall-E 难以理解形状之间的预期关系,导致意外的结果。

Images generated by Dall-E given the prompt: “Draw a vertical line connected to a rectangle, connect a square to the line, and connect the square with another vertical line to another rectangle, and finally connect the rectangle to a circle with another vertical line.”

我清楚地意识到Dall-E无法处理几何约束的提示,我尝试了一个更简单的提示:“创建一个只显示两条正交线的绘图”。这也被证明太困难了。

Dall-E生成的图像以提示:“创建一个只显示两条正交线的绘图”

Dall-E的这种无能让我感到惊讶,但是当我思考像Dall-E这样的模型的工作原理时,这并不令人意外。它基于潜在扩散,这本质上是一个嘈杂的过程,并且不适用于基于约束的精确提示。

接下来,我将展示我生成的图像,并更详细地讨论如何编写类似的代码。

由我的代码生成的单个图像示例。
通过我的代码生成的不同图像的gif,展示了相同参数生成的图像的多样性。

我使用Python和Pillow创建了这些图像,没有使用任何机器学习。通过Torch引入了随机性元素,Torch是一个多用途的包,我利用它的熟悉和便利性。它通常是机器学习(ML)中使用的包。但是这些图像并不是使用机器学习(ML)生成的。

你可能会想知道这些图像的多样性来源于何处,我个人喜欢我的代码如何能够生成具有相似感觉但在仔细观察时又如此不同的图像。输出多样性是实现的一个关键特征。我代码生成的图像的变化来自于对随机变量的精妙使用。在概率论和统计学的领域中,随机变量是其可能值是随机现象结果的变量。

现在,我将描述我的代码生成的图像的生成过程,并在Python中展示一些高层次的示例,展示这个生成过程的样子。

我们可以将生成过程分为3个步骤。

  • 步骤1:生成中心图案。这通过从固定位置采样矩形、线条、矩形、正方形、线条和圆形来完成。这些形状的大小由随机变量确定。
  • 步骤2:从三个不同的分布中采样带有线条和相邻元素的三个聚类。在每个聚类中,放置一定数量的具有不同起始和结束点的竖直线。
  • 步骤3:从聚类的线条中采样并绘制圆形和矩形。
显示单个图像的逐步生成过程的gif。

步骤1

为了理解我的代码中随机变量的作用,考虑我们图像创建过程中的第一步:形成一个以肖像风格的矩形,其高度大于宽度。这个矩形,虽然看似简单,却是随机变量行动的化身。

一个矩形可以分解为四个主要元素:起始的x和y坐标,以及结束的x和y坐标。现在,当从特定分布中选择这些点时,它们就变成了随机变量。但是我们如何决定这些点的范围,或者更具体地说,它们来自哪个分布?答案在于统计学中最常见和最关键的分布之一:正态分布。

由两个参数——均值(μ)和标准差(σ)定义,正态分布在我们的图像生成过程中起着关键作用。均值μ表示分布的中心,因此充当随机变量值围绕其旋转的点。标准差σ量化分布中的离散程度。它决定了随机变量可能具有的值的范围。实质上,较大的标准差会导致所创建图像的多样性更大。

import torchcanvas_height = 1000canvas_width = 1500#循环显示不同的值for i in range(5):    #创建正态分布进行抽样    start_y_dist = torch.distributions.Normal(canvas_height * 0.8, canvas_height * 0.05)    #从分布中抽样    start_y = int(start_y_dist.sample())        #创建正态分布以抽样高度    height_dist = torch.distributions.Normal(canvas_height * 0.2, canvas_height * 0.05)    height = int(height_dist.sample())    end_y = start_y + height    #由于居中,start_x是固定的    start_x = canvas_width // 2    width_dist = torch.distributions.Normal(height * 0.5, height * 0.1)    width = int(width_dist.sample())    end_x = start_x + width    print(f"start_x: {start_x}, end_x: {end_x}, start_y: {start_y}, end_y: {end_y}, width: {width}, height: {height}")

start_x: 750, end_x: 942, start_y: 795, end_y: 1101, width: 192, height: 306start_x: 750, end_x: 835, start_y: 838, end_y: 1023, width: 85, height: 185start_x: 750, end_x: 871, start_y: 861, end_y: 1061, width: 121, height: 200start_x: 750, end_x: 863, start_y: 728, end_y: 962, width: 113, height: 234start_x: 750, end_x: 853, start_y: 812, end_y: 986, width: 103, height: 174

对正方形进行抽样看起来非常相似,我们只需抽样高度或宽度,因为它们是相同的。对圆进行抽样甚至更简单,因为我们只需抽样半径。

在Python中绘制矩形是一个简单的过程,特别是在使用Pillow库时。下面是如何实现的:

from PIL import Image, ImageDraw# 用白色背景创建一个新的图像# 循环绘制矩形for i in range(5):    img = Image.new('RGB', (canvas_width, canvas_height), 'white')    draw = ImageDraw.Draw(img)    # 创建正态分布进行抽样    start_y_dist = torch.distributions.Normal(canvas_height * 0.8, canvas_height * 0.05)    start_y = int(start_y_dist.sample())    height_dist = torch.distributions.Normal(canvas_height * 0.2, canvas_height * 0.05)    height = int(height_dist.sample())    end_y = start_y + height    start_x = canvas_width // 2    width_dist = torch.distributions.Normal(height * 0.5, height * 0.1)    width = int(width_dist.sample())    end_x = start_x + width    # 绘制矩形    draw.rectangle([(start_x, start_y), (end_x, end_y)], outline='black')    img.show()

第2步

在这些图像的垂直线的背景下,我们考虑三个随机变量,分别是:

  1. 线的起始y坐标(y_start)
  2. 线的结束y坐标(y_end)
  3. 线的x坐标(x)

由于我们处理的是垂直线,每条线只需要抽样一个x坐标。线的宽度是固定的,由画布的大小控制。

为了确保线条不相交,需要一些额外的逻辑来跟踪图像作为网格的占用位置。为了简单起见,我们将忽略这一点。

下面是在Python中的示例。

import torchfrom PIL import Image, ImageDraw# 设置画布的大小canvas_size = 1000# 线的数量num_lines = 10# 为起始和结束y坐标以及x坐标创建分布y_start_distribution = torch.distributions.Normal(canvas_size / 2, canvas_size / 4)y_end_distribution = torch.distributions.Normal(canvas_size / 2, canvas_size / 4)x_distribution = torch.distributions.Normal(canvas_size / 2, canvas_size / 4)# 对每条线从分布中抽样y_start_points = y_start_distribution.sample((num_lines,))y_end_points = y_end_distribution.sample((num_lines,))x_points = x_distribution.sample((num_lines,))# 创建白色画布image = Image.new('RGB', (canvas_size, canvas_size), 'white')draw = ImageDraw.Draw(image)# 绘制线条for i in range(num_lines):    draw.line([(x_points[i], y_start_points[i]), (x_points[i], y_end_points[i])], fill='black')# 显示图像image.show()

然而,这只给你线条。集群的另一部分是线条末端的圆圈,我称之为相邻圆圈。随机变量也决定了它们的过程。首先,相邻圆圈的存在性从伯努利分布中进行抽样,形状的位置(左、中、右)从均匀分布中进行抽样。

圆圈可以完全由一个参数来定义:它的半径。我们可以将线条的长度视为影响圆圈半径的条件。这构成了一个条件概率模型,其中圆圈的半径(R)取决于线条的长度(L)。我们使用条件高斯分布。该分布的均值(μ)是线条长度的平方根的函数,而标准差(σ)是一个常数。

我们最初建议,在给定线条长度L的情况下,半径R服从正态分布。这表示为R | L ~ N(μ(L), σ²),其中N是正态(高斯)分布,σ是标准差。

然而,这存在一个小问题:正态分布包括可能抽样负值的可能性。在我们的场景中,这种结果在物理上是不可能的,因为半径不能为负。

为了解决这个问题,我们可以使用半正态分布。这个分布与正态分布非常类似,由一个尺度参数σ定义,但关键的是,它被限制在非负值上。给定线条长度的半正态分布:R | L ~ HN(σ),其中HN表示半正态分布。这样,σ由所需的均值确定,即σ = √(2L) / √(2/π),确保所有抽样的半径都是非负的,并且分布的均值为√(2L)

from PIL import Image, ImageDrawimport numpy as npimport torch# 定义线条长度L = 3000# 计算半正态分布的期望mu = np.sqrt(L * 2)# 计算给定期望的尺度参数scale = mu / np.sqrt(2 / np.pi)# 使用计算得到的尺度参数创建半正态分布dist = torch.distributions.HalfNormal(scale / 3)# 抽样并绘制多个圆圈for _ in range(10):    # 创建一个白色背景的新图像    img_size = (2000, 2000)    img = Image.new('RGB', img_size, (255, 255, 255))    draw = ImageDraw.Draw(img)    # 定义圆圈的中心    start_x = img_size[0] // 2    start_y = img_size[1] // 2    # 从分布中抽样半径    r = int(dist.sample())    print(f"抽样的半径:{r}")    # 定义圆圈的边界框    bbox = [start_x - r, start_y - r, start_x + r, start_y + r]    # 在图像上绘制圆圈    draw.ellipse(bbox, outline ='black',fill=(0, 0, 0))    # 显示图像    img.show()

步骤3

我们的第3步是步骤1和步骤2的元素的组合。在步骤1中,我们解决了在固定位置抽样和绘制矩形的任务。在步骤2中,我们学习了如何使用正态分布在您的画布的一部分上绘制线条。此外,我们还学会了如何抽样和绘制圆圈。

当我们转向步骤3时,我们将重新利用之前步骤中使用的技术。我们的目标是在我们之前抽样的线条周围和谐地分布正方形和圆圈。正态分布将再次派上用场。

我们将重用用于创建线条集群的参数。然而,为了增强视觉吸引力并避免重叠,我们对均值(mu)和标准差值引入一些噪音。

在此步骤中,我们的任务不再是定位线条,而是放置抽样的矩形和圆圈。我鼓励您尝试使用这些技术,看看是否可以将圆圈和矩形添加到您的线条集群中。

在这篇博文中,我对我的代码进行了解剖和简化,以便更深入地理解它的运行方式。我展示了像Dall-E这样的生成AI模型遵循精确约束的困难。

编写能够生成这些图像的代码对我来说是一次很棒的经历。看到我编写的每一行代码都让图像进展一步,是一件很酷的事情。我希望这篇博文引起了您对艺术和编码交叉领域的兴趣。我鼓励您利用自己的编码技能,用代码将您的想象力变为现实。您无需耗尽Dall-E的信用,创造的力量就在您的指尖。