Как стать автором
Обновить

Код как кисть: как Python превращается в художника с помощью генеративных алгоритмов

Уровень сложностиСложный
Время на прочтение4 мин
Количество просмотров2.7K

Можно ли научить 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")

Финальный аккорд: собрать всё вместе

Создание генеративного искусства — это как варка борща: важно не столько следовать рецепту, сколько пробовать на вкус. Хочется больше красного? Убавь шум. Не хватает контраста? Прогони через градиент.

Вот пример итогового подхода:

  1. Генерируем текстуру шума — фон

  2. Добавляем несколько цветовых пятен

  3. Поверх — линии или фигуры

  4. Всё — сохраняем и радуемся


Личное: а зачем вообще всё это?

Иногда очень полезно отвлечься от задач, которые «имеют смысл». Генеративное искусство — это способ побыть в потоке, поиграть с хаосом и логикой одновременно. Это момент, когда математика перестаёт быть скучной, а код — просто инструментом.

Кто-то скажет: «А зачем? Это же бесполезно». Но в том-то и дело. Это красиво. А значит — полезно.


Заключение

Программирование не обязано быть только про производительность и бенчмарки. Оно может быть про чувство. Про визуальный кайф. Про баг, который стал картиной.

Python — это не просто язык, это кисть. И как её использовать — зависит только от фантазии.

Если после этой статьи ты открыл PIL, попробовал сгенерировать хоть что-то, и на секунду почувствовал себя художником — значит, всё не зря.


P.S. Можно добавить генерацию GIF, анимацию, или даже использовать tkinter для интерактива. Но это уже совсем другая история.

Теги:
Хабы:
+9
Комментарии5

Публикации

Работа

Data Scientist
41 вакансия

Ближайшие события