2025, Sep 14 07:50

Как согласовать координаты для множества Мандельброта в Python и PIL

Почему фрактал Мандельброта в Python/PIL выглядит разрезанным на зеркальные квадранты и как это исправить: согласование координат, центр, масштаб и смещения.

При рисовании множества Мандельброта на Python с PIL распространённым первым симптомом ошибки отображения координат становится картинка, будто разрезанная на четыре части, где каждый квадрант зеркально отражён или перевёрнут. С формулами всё в порядке, с цветами тоже — проблема в том, что комплексная плоскость и буфер изображения по‑разному понимают, где находится начало координат.

Пример кода

from PIL import Image
from PIL import ImageShow
from PIL import ImageColor as ColorMap
palette = ["navy","darkblue","blue","cornflowerblue","lightsteelblue","lightskyblue","turquoise","palegreen","lawngreen"]
def iterate_point(c, scale):
    z = 0.0
    iter_count = 0
    iter_cap = 5*scale
    while z.real == z.real and iter_count < iter_cap:
        z = (z*z) + c
        iter_count = iter_count + 1
    if z.real == z.real:
        return -1
    else:
        return iter_count
def run_app():
    scale = int(input("Set Zoom Level. (Default: 10)     ") or "10")*10
    cx = int(input("Input centre x value. (Default: -50)     ") or "-50")
    cy = int(input("Input centre y value. (Default: 0)     ") or "0")
    w2 = canvas.width // 2
    h2 = canvas.height // 2
    for py in range((cy - h2),(cy + h2)):
        for px in range((cx - w2),(cx + w2)):
            c = complex((px/scale),(py/scale))
            steps = iterate_point(c,scale)
            if steps == -1:
                rgb = (0,0,0)
            else:
                rgb = ColorMap.getrgb(palette[steps % len(palette)])
            canvas.putpixel(((px + w2 - cx) % canvas.width,
                             (py + h2 - cy) % canvas.height),
                            rgb)
        print("line",(py+h2),"done")
    ImageShow.show(canvas)
    run_app()
canvas = Image.new("RGB",(1000,500),"Black")
run_app()

Суть проблемы

Функция, определяющая цвет для каждой точки на комплексной плоскости, работает с множеством Мандельброта, центрированным в complex(0, 0). Так обычно и задаётся математика: начало координат — в центре комплексной плоскости, X — это действительная ось, а мнимая идёт вертикально. Буфер изображения, напротив, считает началом координат верхний левый пиксель. Если напрямую переводить комплексные координаты в индексы пикселей, не согласовав эти две системы, получится картинка как будто разрезанная и зеркальная — каждый квадрант окажется в «чужой» части холста.

Есть и второй слой: вы можете смещать область просмотра с помощью пользовательского ввода центра. Это смещение нужно применять последовательно и при обходе комплексной плоскости, и при записи пикселей в изображение, иначе появятся сдвиги или «заворот» по краям.

Решение

Нужно совместить математический центр с визуальным. Перенесите начало координат пиксельного пространства из верхнего левого угла в середину изображения, используя половины ширины и высоты. Затем учтите заданное пользователем смещение центра как при итерации, так и при записи на холст. На практике это значит: проходить диапазоны, центрированные на выбранных смещениях, переводить переменные цикла в комплексные координаты с учётом текущего масштаба и ставить пиксели по позициям, сдвинутым на половину размеров изображения с вычитанием того же смещения. Приведённый выше код делает именно это: вычисляет полу-ширину и полу-высоту по фактическому размеру изображения, получает комплексные координаты через scale и размещает пиксели по скорректированным индексам.

Попутно две прагматичные правки делают поведение программы предсказуемее. Поля ввода действительно уважают значения по умолчанию: берётся то, что ввёл пользователь, или строковый дефолт, который безопасно приводится к int. Размер изображения больше не зашит в логику, поскольку половинные размеры берутся с холста — менять разрешение можно где угодно, не ломая координатную математику.

Есть и небольшое усиление надёжности внутри итерации. Вместо разбора строк для поиска некорректных значений код опирается на простое свойство: NaN не равен сам себе. Проверка z.real == z.real — компактный способ продолжать цикл, пока значение нормальное, и остановиться, когда это уже не так, избегая лишних преобразований строк.

Почему это важно

Любой алгоритм, переводящий математическую область в пиксели, держится на корректном преобразовании координат. Множество Мандельброта особенно чувствительно: симметрия мгновенно выдаёт ошибки — сдвинутое или отражённое начало координат сразу «ломает» знакомые кардиоиду и «пузыри». Точный центр, последовательное применение смещений и выбор окон итерации по реальному размеру изображения — малые шаги, которые предотвращают артефакты и упрощают дальнейшую работу, будь то навигация или настройка палитры.

На комплексной плоскости ось X — это действительные числа, а вертикальная ось — мнимые. Держась этой модели, проще не запутаться в отображении при панорамировании и масштабировании.

Итоги

Если картинка множества Мандельброта выглядит как зеркальные квадранты, согласуйте системы координат. Совместите начало комплексной плоскости с центром изображения, последовательно вычитайте заданные пользователем смещения и вычисляйте границы из размеров изображения, а не жёстко прописывайте их. Сделайте значения по умолчанию действительно значениями по умолчанию и предпочитайте прямые числовые проверки на NaN. С этими элементами фрактал проявится как задумано, а у вас появится чистая база для дальнейших улучшений. Вдобавок, если вертикальное смещение не нужно, вертикальную симметрию множества можно использовать, чтобы сократить работу, просчитывая лишь половину строк. Ключевое — правильно настроить отображение; дальше всё становится значительно проще.

Статья основана на вопросе с StackOverflow от Jessica и ответе от bruno.