Поскольку программа будет содержать сообщения на русском языке, включим режим UTF-8 и позаботимся о соответствующих настройках стандартных дескрипторов:
Perluse utf8; use open qw/:locale/;
Затем подключим нужные модули. Для создания графического файла потребуется
знакомый нам по главе 47. «Трассировка растровых изображений» модуль
Image::Magick
, а для обработки опций в командной
строке — модуль Getopt::Long
:
Perluse Image::Magick; use Getopt::Long qw/:config no_ignore_case bundling/;
Предусмотрим в программе возможность задания в командной строке следующих опций:
-o
, --output-file
имя
графического файла
Имя выходного графического файла; по умолчанию
'Mandelbrot.png'
.
-r
, --region
umin
:vmin
:umax
:vmax
Прямоугольная область на комплексной плоскости, которая должна быть изображена;
задаётся неравенствами
,
,
где
.
Всё множество Мандельброта хорошо помещается в прямоугольник
,
,
так что сделаем для этой опции значение по умолчанию равным
'-2.4:-1.2:.8:1.2'
.
-d
, --density
число
Количество пикселов на единицу длины, по умолчанию 400
.
-R
, --radius
число
Радиус такого круга, что содержащиеся в нём орбиты мы считаем практически
ограниченными, по умолчанию 100
.
-h
, --help
Показать подсказку.
Для значений опций объявим переменные $outputFile
,
$region
, $density
,
$radius
и присвоим им значения по умолчанию. Кроме того,
определим процедуру help
, которая печатает подсказку:
Perlmy $radius=100; my $outputFile='Mandelbrot.png'; my $region='-2.4:-1.2:.8:1.2'; my $density=400; sub help() { STDERR->print(<<__HELP__); mandelbrot.pl ⟨опции⟩ ⟨опции⟩: -o, --output-file ⟨имя графического файла⟩ имя выходного файла («$outputFile») -r, --region ⟨umin⟩:⟨vmin⟩:⟨umax⟩:⟨vmax⟩ прямоугольник на комплексной плоскости ($region) -d, --density ⟨целое число⟩ количество пикселов на единичном отрезке ($density) -R, --radius ⟨вещественное число⟩ радиус ($radius) -h, --help показать эту подсказку __HELP__ exit; }
Следующим шагом будет извлечение опций из командной строки:
PerlGetOptions ( 'o|output-file=s' =>\$outputFile, 'r|region=s' =>\$region, 'd|density=f' =>\$density, 'R|radius=f' =>\$radius, 'h|help' =>\&help, );
Теперь извлечём из значения опции --region
отдельные числовые
компоненты при помощи split
и разместим их в четырёх
переменных с говорящими именами:
Perlmy ($left, $bottom, $right, $top)=split ':', $region;
Нам будет удобнее оперировать не с радиусом, а с его квадратом, поэтому будет разумным вычислить этот квадрат:
Perlmy $squaredRadius=$radius**2;
После всех этих приготовлений можно приступать к содержательной части программы.
Ключевой в программе будет процедура iterate
, которая по
двум вещественным числам и определяет цвет, которым должна быть
закрашена точка
.
Как и планировалось, мы применяем метод Флойда. Черепаха, заданная как пара
переменных $tx
и $ty
, побежит по орбите.
Заяц тоже, но с удвоенной скоростью. Остановится цикл в одном из двух случаев:
либо черепаха выйдет за пределы круга радиуса , либо Черепаха и Заяц сблизятся на
расстояние, меньшее чем
.
От этого будет зависеть знак возвращаемого процедурой значения. Разумеется,
с каждым проходом цикла должна увеличиваться переменная $n
,
подсчитывающая длину черепашьей орбиты. Эта длина также используется для
нахождения цвета:
Perlsub iterate($$) { my ($u, $v)=@_; my ($tx, $ty)=my ($hx, $hy)=(0, 0); my $n=0; while() { ($tx, $ty)=($tx**2-$ty**2+$u, 2*$tx*$ty+$v); ($hx, $hy)=($hx**2-$hy**2+$u, 2*$hx*$hy+$v) for 0, 1; return 1/sqrt($n/4+1) if $tx**2+$ty**2>=$squaredRadius; return -1/sqrt($n/4+1) if (($tx-$hx)**2+($ty-$hy)**2)*$squaredRadius<1; $n++; } }
Настало время создать графический файл. При создании потребуется указать
размеры изображения в пикселах. Для нахождения размеров нужно взять целую часть
каждого из размеров прямоугольника на комплексной плоскости, умноженного на
величину $density
.
Perlmy ($w, $h)=map { int($density*$_) } ($right-$left, $top-$bottom);
Теперь в переменных $w
и $h
оказались
ширина и высота изображения в пикселах. Можно создавать новое, пока ещё целиком
чёрное изображение, применяя методы из класса
Image::Magick
:
Perlmy $image=Image::Magick->new(size=>"${w}x$h"); $image->ReadImage('NULL:black'); $image->Color(color=>'black'); $image->Comment("region=$region density=$density radius=$radius"); $image->Set(depth=>8);
Вторая и третья строчки выглядят как некая чёрная магия. Без второй изображение не удаётся сохранить в файле. Без третьей изображение сохраняется, но оказывается полностью прозрачным. Не ломая голову над смыслом этих двух команд, мы позаимствовали их из многочисленных примеров, размещённых на сайте проекта ImageMagick. Программа заработала, и, как говорится, слава богу.
Метод Comment
прикладывает к изображению текстовый
комментарий. Мы воспользовались этой возможностью, чтобы вместе с картинкой
сохранить значения важных опций, использовавшихся при создании изображения.
Наконец, метод Set
позволяет устанавливать различные
свойства изображения. Мы устанавливаем свойство depth
(глубину). Глубиной изображения
называют количество бит на один пиксел. Количество различных цветовых значений,
которые может принимать пиксел, можно получить, возведя двойку в степень,
равную глубине. Стандартным значением глубины для формата
PNG является 24 бита
(TrueColor), то есть по 8 бит на каждую цветовую
компоненту — красную, зелёную и синюю. Мы же выбрали глубину в восемь бит,
надеясь, и не без оснований, уменьшить размер получившегося графического файла.
В режиме TrueColor каждый пиксел изображения кодируется как три байта, где в каждом байте записана интенсивность красной, зелёной и синей компоненты. При глубине 8 применяется другой подход. К изображению прилагается массив (палитра), в котором перечислены все использованные цвета в 24-битном формате. Этих цветов не более 256, так что размер палитры получается небольшим (максимум 768 байт). Каждый пиксел изображения при этом кодируется как восьмибитный номер цвета из палитры. Этот режим называется Palette, или индексированным.
Может показаться, что размер файла при восьмибитной глубине уменьшается втрое, но это не так. Растровые изображения обычно подвергаются сжатию, из-за чего экономия может отличаться от трёхкратной. Кроме того, графический файл может содержать вспомогательную информацию: палитру, комментарии, сведения о программном обеспечении, использованном при создании, и всякое другое.
Разумеется, уменьшение глубины делает картинку более бедной: вместо возможных цветов придётся обойтись лишь . Но особенность нашей задачи такова, что уменьшение количества цветов остаётся незаметным для невооружённого глаза. Чтобы читатель мог самостоятельно оценить влияние глубины на качество картинки и размер файла, мы предлагаем рисунок 48.3. «Растровое изображение в режимах TrueColor и Palette» нашей любимой железобетонной роншанской капеллы Нотр-дам-дю-о. Левое изображение закодировано в режиме TrueColor и весит 106553 байта, а правое — в режиме Palette (31499 байт).
Но мы сильно отвлеклись от программирования, а осталось не так много работы: рисование и сохранение результата в файле.
Рисование производится при помощи двух вложенных циклов: во внешнем изменяется номер столбца, а во внутреннем — строки:
Perlfor my $i(0..$w-1) { printf "\e[K\rГотово: \%d\%\%", $i/$w*100; STDOUT->flush; for my $j(0..$h-1) { вычислить $color $image->SetPixel(x=>$i, y=>$j, color=>$color); } }
Перед рисованием очередного столбца выводится сообщение для пользователя
о степени готовности рисунка. Чтобы сообщения не занимали много места на
экране, мы выводим их в одной и той же строке. Чтобы добиться этого, мы должны
каждый раз очищать строку и перемещать курсор в её начало. За первое действие
отвечает управляющая последовательность "\e[K"
, а за
второе — символ возврата каретки "\r"
. В конце сообщения
не выводится символ конца строки, поэтому, если мы специально не позаботимся,
сообщения будут буферизоваться и появляться на экране не своевременно. Для
борьбы с этим явлением станем принудительно сбрасывать буфер вывода командой
STDOUT->flush
.
Вставка, которую мы отложили на потом, могла быть запрограммирована следующим образом:
Perlmy $value=iterate($i/$density+$left, -$j/$density+$top); my $color=$value<0? [-$value, 0, 0]: [0, $value, 0];
В первой строчке номера столбца и строки преобразуются в вещественную и мнимую
части комплексного числа, которые передаются процедуре
iterate
. В зависимости от знака возвращённого значения
строится цвет как ссылка на трёхэлементный массив с цветовыми компонентами.
Внимательно изучим формулы перехода от координат на комплексной плоскости к пиксельным координатам . Имеем («лишний» знак минус во втором равенстве появляется из-за того, что ось ординат на комплексной плоскости направлена вверх, а на картинке — вниз). Отсюда получаем
Но мы собирались использовать симметричность множества Мандельброта для
ускорения работы. Поэтому код вставки усложнится. По номеру строки вычислим номер «комплексно сопряжённой»
строки
.
Для этого сначала найдём номер строки, соответствующей вещественной оси.
Подставим
в формулы, связывающие две координатных системы, и найдём, что номер «нулевой»
строки равен
.
Поскольку «комплексно сопряжённые» строки на рисунке должны быть симметричными
относительно «нулевой», получаем равенство
,
откуда
.
Отправим вычисленный номер «комплексно сопряжённой» строки в переменную
$jConjugate
и приготовим переменную для цвета:
Perlmy $jConjugate=2*$density*$top-$j; my $color;
Условия, при которых можно скопировать пиксел вместо вычисления его цвета при
помощи iterate
, выражаются двумя неравенствами на
:
и
.
Первое из них означает, что пиксел, «комплексно сопряжённый» текущему,
находится в пределах изображения. Второе неравенство требует, чтобы цвет
«комплексно сопряжённого» пиксела был вычислен раньше, ведь пикселы
обрабатываются в порядке увеличения номера строки.
Все эти соображения приводят к коду
Perlif($jConjugate<$j and $jConjugate>=0) { $color=[$image->GetPixel(x=>$i, y=>$jConjugate)]; } else { my $value=iterate($i/$density+$left, -$j/$density+$top); $color=$value<0? [-$value, 0, 0]: [0, $value, 0]; }
Остаток программы посвящается сохранению изображения в файл. Метод
Write
в случае каких-нибудь неполадок возвращает
непустую строку с сообщением об ошибке. Мы проверяем, всё ли в порядке,
и выводим сообщение, соответствующее случаю.
Perlif((my $message=$image->Write($outputFile)) eq '') { my $size=-s $outputFile; print "\e[K\rФайл «$outputFile» (${w}×$h, $size байт) записан.\n"; } else { die "\n$message\n"; }
Программа готова.