Можно ли научить Python рисовать? Эта статья — не сухой туториал, а настоящая история экспериментов с генеративным искусством. В ней рассказывается о создании цифровых картин с помощью случайностей, математики, шума Перлина и любви к визуальному абсурду. Много кода, немного философии и никакой нейросети — только чистый Python и жажда выразительности.

Порой программисту хочется отдохнуть от API, CRUD и фреймворков, и попробовать что-то бессмысленное, но красивое. Вот как программисты становятся художниками. Кто-то берет Blender, кто-то — нейросети. А кто-то — открывает matplotlib
и начинает играться со случайными числами.
Сначала всё выглядит как чёртов бардак. А потом вдруг — бах! — появляется текстура, от которой не отвести глаз. Именно так начался этот эксперимент. Ни плана, ни цели. Только желание рисовать с помощью кода.
Идея проста: сгенерировать изображение с нуля, используя только Python и немного хаоса. Никаких pre-trained моделей, никакого ML. Только математические функции и воображение. Это — код как кисть.
Что понадобится
Python 3.10+
Библиотеки:
numpy
,matplotlib
,Pillow
,noise
pip install numpy matplotlib Pillow noise
Первый мазок: случайные цвета и шум
Цифровое полотно — это просто массив. Каждый пиксель — это точка с координатами и цветом. Начнем с создания цветной карты с использованием шума Перлина.
import numpy as np
from PIL import Image
from noise import pnoise2
width, height = 800, 800
scale = 100.0
image = Image.new("RGB", (width, height))
pixels = image.load()
for x in range(width):
for y in range(height):
nx = x / scale
ny = y / scale
value = pnoise2(nx, ny, octaves=6)
r = int((value + 0.5) * 255) % 256
g = int((0.5 - value) * 255) % 256
b = (x * y) % 256
pixels[x, y] = (r, g, b)
image.save("perlin_art.png")
Шум Перлина позволяет избежать классической зернистости случайных чисел и создаёт «органичную» текстуру.
Углубляемся: цветовые поля с контролем хаоса
Иногда картине не хватает композиции. Добавим цветовые поля, ограниченные по форме, но заполненные шумом. Это похоже на эксперимент в стиле Марка Ротко, только для интровертов с клавиатурой.
def draw_colored_field(image, center, radius, base_color, scale=30.0):
pixels = image.load()
cx, cy = center
for x in range(image.width):
for y in range(image.height):
dx, dy = x - cx, y - cy
if dx * dx + dy * dy <= radius * radius:
nx, ny = x / scale, y / scale
noise_val = pnoise2(nx, ny, octaves=4)
r = int((base_color[0] + noise_val * 50) % 256)
g = int((base_color[1] + noise_val * 50) % 256)
b = int((base_color[2] + noise_val * 50) % 256)
pixels[x, y] = (r, g, b)
image = Image.new("RGB", (800, 800), color="white")
draw_colored_field(image, (400, 400), 250, (100, 50, 200))
image.save("field_art.png")
Можно нарисовать несколько полей с разными цветами — и получить сюрреалистичный пейзаж.
Штрихи — линии и повторения
Настоящий художник знает цену повтору. Программист может зациклить его. Добавим абстрактные линии, создающие ощущение ритма.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(0, 100)
ax.set_ylim(0, 100)
for i in range(50):
x = np.linspace(0, 100, 1000)
y = 50 + 20 * np.sin(x * 0.1 + i)
ax.plot(x, y, alpha=0.1, color='black')
ax.axis('off')
plt.savefig("lines_art.png", bbox_inches='tight', pad_inches=0)
Дополнительно: фрактальные формы
Иногда простая рекурсия может нарисовать нечто удивительное. Пример — дерево, где каждая ветка порождает новые ветки:
from PIL import Image, ImageDraw
import math
def draw_branch(draw, x, y, angle, depth):
if depth == 0:
return
x2 = x + int(math.cos(angle) * depth * 10.0)
y2 = y + int(math.sin(angle) * depth * 10.0)
draw.line((x, y, x2, y2), fill=(0, 100 + depth * 15, 0), width=depth)
draw_branch(draw, x2, y2, angle - 0.3, depth - 1)
draw_branch(draw, x2, y2, angle + 0.3, depth - 1)
img = Image.new("RGB", (800, 600), (255, 255, 255))
draw = ImageDraw.Draw(img)
draw_branch(draw, 400, 500, -math.pi / 2, 10)
img.save("tree_fractal.png")
Дополнительно: генерация лабиринта
А что если рисовать нечто строгое, но всё ещё случайное? Например, лабиринт по алгоритму depth-first search.
import random
from PIL import Image, ImageDraw
width, height = 40, 40
cell_size = 20
maze = [[0] * width for _ in range(height)]
stack = [(0, 0)]
visited = set(stack)
def neighbors(x, y):
for dx, dy in [(0, -1), (1, 0), (0, 1), (-1, 0)]:
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height and (nx, ny) not in visited:
yield nx, ny
while stack:
x, y = stack[-1]
next_cells = list(neighbors(x, y))
if next_cells:
nx, ny = random.choice(next_cells)
visited.add((nx, ny))
stack.append((nx, ny))
maze[ny][nx] = 1
else:
stack.pop()
img = Image.new("RGB", (width * cell_size, height * cell_size), "white")
draw = ImageDraw.Draw(img)
for y in range(height):
for x in range(width):
if maze[y][x] == 0:
draw.rectangle(
[x * cell_size, y * cell_size, (x+1) * cell_size, (y+1) * cell_size], fill="black")
img.save("maze.png")
Дополнительно: пиксельные узоры по формулам
Иногда достаточно просто поиграть с уравнениями. Например, построим картину из уравнений:
from PIL import Image
import math
w, h = 600, 600
img = Image.new("RGB", (w, h))
pixels = img.load()
for x in range(w):
for y in range(h):
r = int((math.sin(x * 0.01) + 1) * 127)
g = int((math.cos(y * 0.01) + 1) * 127)
b = int((math.sin((x + y) * 0.01) + 1) * 127)
pixels[x, y] = (r, g, b)
img.save("formula_art.png")
Финальный аккорд: собрать всё вместе
Создание генеративного искусства — это как варка борща: важно не столько следовать рецепту, сколько пробовать на вкус. Хочется больше красного? Убавь шум. Не хватает контраста? Прогони через градиент.
Вот пример итогового подхода:
Генерируем текстуру шума — фон
Добавляем несколько цветовых пятен
Поверх — линии или фигуры
Всё — сохраняем и радуемся
Личное: а зачем вообще всё это?
Иногда очень полезно отвлечься от задач, которые «имеют смысл». Генеративное искусство — это способ побыть в потоке, поиграть с хаосом и логикой одновременно. Это момент, когда математика перестаёт быть скучной, а код — просто инструментом.
Кто-то скажет: «А зачем? Это же бесполезно». Но в том-то и дело. Это красиво. А значит — полезно.
Заключение
Программирование не обязано быть только про производительность и бенчмарки. Оно может быть про чувство. Про визуальный кайф. Про баг, который стал картиной.
Python — это не просто язык, это кисть. И как её использовать — зависит только от фантазии.
Если после этой статьи ты открыл PIL
, попробовал сгенерировать хоть что-то, и на секунду почувствовал себя художником — значит, всё не зря.
P.S. Можно добавить генерацию GIF, анимацию, или даже использовать tkinter
для интерактива. Но это уже совсем другая история.