dlib 0.5

Не так давно состоялось очередное крупное обновление коллекции библиотек dlib — вышла версия 0.5, наиболее значительным нововведением которой стала поддержка ручного управления памятью (РУП). Но — обо всем по порядку…

  • Новый модуль dlib.core.memory предоставляет средства для ручного выделения и высвобождения динамической памяти, независимые от сборщика мусора и основанные на malloc/free. Имеется поддержка структур, классов и массивов. При использовании классов рекомендуется использовать интерфейс ManuallyAllocatable и перегружать метод free, который ответственен за удаление объекта — в противном случае корректное удаление в некоторых случаях не гарантировано (например, при доступе через интерфейс или родительский класс).
  • Началась работа по переводу всей dlib на РУП. Так, загрузчики изрбражений (PNG, JPEG, TGA, BMP) в новой версии полностью независимы от сборщика мусора. Для этого активно используется паттерн абстрактной фабрики, ответственный за создание изображений  в памяти. Кстати, в загрузчике PNG значительно улучшена поддержка индексированных изображений, для них добавлена поддержка альфа-канала.
  • Кроме того, на РУП переведены некоторые контейнеры из dlib.container — BST, ассоциативный массив. Реализован полностью ручной динамический массив (dlib.container.array).
  • Еще одна новинка — ООП для структур (dlib.core.oop). Это экспериментальный модуль, реализующий для структур прототипный стиль ООП с поддержкой множественного наследования и параметрического полиморфизма. Полностью заменить классы он, конечно, не может, но окажется весьма полезен, если нужно создавать объекты с наследованием в стеке. В будущем планируется переписать некоторые внутренние механизмы dlib с использованием этой легковесной объектной системы.
  • В пакете dlib.math появилась поддержка дуальных кватернионов. Это частный случай алгербы Клиффорда, обобщение кватернионов на поле дуальных чисел. Их можно использовать, например, для описания движения тел в кинематике — один дуальный кватернион охватывает и перенос, и вращение. Кстати, реализация обычных кватернионов через инкапсуляцию теперь совместима с векторами.
  • Изменения коснулись и пакета вычислительной геометрии. Усеченная пирамида (dlib.geometry.frustum) теперь задается с нормалями ограничивающих плоскостей, указывающими наружу пирамиды. Подвергся изменению API проверки пересечения Frustum с AABB. Исправлены ошибки в реализации AABB и плоскости.

Итоги года

Вот и пролетел еще один год — самое время подводить итоги по проделанной работе!

  • Вышли подряд нескольно новых версий dlib (0.3 и 0.4). Появилась поддержка абстрактных потоков ввода/вывода, а также платформонезависимый интерфейс файловой системы и его реализации для Windows и POSIX. Пакет обработки изображений теперь поддерживает JPEG, TGA и BMP, распараллеливание, HDRI. В пакете линейной алгебры состоялся серьезный рефакторинг матриц, появилась поддержка инверсии через LU-разложение.
  • Было выпущено 6 номеров электронно-познавательного журнала «FPS» (№№ 28, 29, 30, 31, 32, 33). Появился новый сайт проекта (http://fps-magazine.cf). Также «FPS» теперь доступен в качестве мобильного приложения для Android и iOS. Кстати, в феврале 2015 года журналу исполняется уже 7 лет!
  • Вышла игра 2048х2 — клон 2048 для двух игроков.
  • Улучшен физический движок dmech: реализован новый кэш контактов, добавлена поддержка составных тел, улучшена поддержка ограничений, добавлены статические тримеши, поддержка raycast и игровой кинематики.
  • Графический движок Atrium теперь развивается как самостоятельный проект — DGL. Это объектно-ориентированная надстройка над OpenGL, SDL и Freetype с собственной системой событий, виртуальной файловой системой с поддержкой ZIP-архивов, своим форматом хранения сцен, поддержкой шейдеров, мультитекстурирования, скелетной анимации, выводом текста в UTF-8, а также встроенными средствами интернационализации.
  • Разработан скриптовый язык GScript — минималистичный императивный язык с динамической типизацией, идейно близкий к D, JavaScript и Python. GScript можно будет использовать в качестве скриптовой системы в игровых движках.
  • Вышла новая версия системы сборки проектов Cook 2.0.1 — с новой системой аргументов командной строки, обновленным парсером импортов, поддержкой внешних зависимостей (в том числе из Git-репозиториев), улучшенной системой конфигурации.
  • Обновилась страница проекта Atrium.

Поддержка JPEG в dlib

Коллекция библиотек dlib обзавелась начальной поддержкой декодирования формата JPEG (dlib.image.io.jpeg). Пока поддерживается только baseline-часть стандарта, декодер читает только изображения с прореживанием 4:2:0 и не загружает метаданные EXIF (эти ограничения постепенно будут исправлены). Как и другие декодеры графических форматов в dlib, модуль работает на основе абстрактных потоков ввода/вывода (dlib.core.stream).
Поддержка сохранения в JPEG в ближайшем будущем не планируется.

Chroma Key с использованием dlib

Эффект Chroma Key (“цветовой ключ”) заключается в сегментации изображения с тем, чтобы отделить объект переднего плана от фона. При этом цвет фона должен быть сплошным и равномерным – как правило, выбирают либо зеленый, либо синий, в зависимости от того, какой цвет отсутствует на объекте. Отделенное изображение затем накладывается на другой фон – например, на фотографию или рендер виртуальной сцены.

Существуют различные алгоритмы подобной сегментации, мы рассмотрим один из самых простых. Несмотря на простоту, он достаточно эффективен. Метод основан на нахождении евклидового расстояния в пространстве RGB – между цветом исходного пикселя и цветом фона. Если рассматривать цвета как точки в трехмерном пространстве, то пиксели, например, зеленого фона будут представлять собой облако точек, сосредоточенное вокруг “абсолютно зеленой” точки – (0, 1, 0). Чтобы получить значение альфа-канала (0 – пиксель принадлежит фону, 1 – не принадлежит), мы просто нормируем расстояние в заранее выбранном диапазоне.

import dlib.math.vector;
import dlib.math.utils;
import dlib.image.image;
import dlib.image.color;

SuperImage chromaKey(
    SuperImage img, 
    Color4f keyColor, 
    float minDist,
    float maxDist)
{
    auto res = new ImageRGBA8(img.width, img.height);
   
    foreach(y; img.col)
    foreach(x; img.row)
    {       
        Color4f col = img[x, y];
        
        Color4f delta = col - keyColor;
        float distSqr = dot(delta, delta);
        col.a = clamp(
            (distSqr - minDist) / (maxDist - minDist), 
            0.0f, 1.0f);
        res[x, y] = col;
    }
    
    return res;
}

Вот пример использования этой функции:

import dlib.image.io.io;

auto img = load("input.png");
auto res = img.chromaKey(Color4f(0, 1, 0), 0.3f, 0.7f);
res.save("output.png");

Как нетрудно заметить, результат не идеален – если наложить изображение на фон, вокруг актера наблюдается зеленоватый контур. От него можно избавиться путем эрозии альфа-канала: изображение пропускается через дискретный оконный фильтр 3х3, который присваивает пикселю наименьшее значение в окне. В результате, непрозрачная область “теряет” несколько пикселей контура, и зеленый ореол практически исчезает.

SuperImage erodeAlpha(SuperImage img)
{
    uint kw = 3, kh = 3;
    
    auto res = img.dup;
    
    foreach(y; img.col)
    foreach(x; img.row)
    {
        auto c = img[x, y];
        
        foreach(ky; 0..kh)
        foreach(kx; 0..kw)
        {
            int iy = y + (ky - kh/2);
            int ix = x + (kx - kw/2);

            if (ix < 0) ix = 0;
            if (ix >= img.width) ix = img.width - 1;
            if (iy < 0) iy = 0;
            if (iy >= img.height) iy = img.height - 1;
            
            float a = img[ix, iy].a;
            
            if (a < c.a) 
                c.a = a;
        }

        res[x, y] = c;
    }
    
    return res;
}

dlib 0.3

Состоялся релиз коллекции библиотек dlib 0.3. Нововведения этой версии:

  • Добавлены абстрактные потоки ввода/вывода (dlib.core.stream), независимые от Phobos, а также интерфейс файловой системы (dlib.filesystem) с готовыми реализациями для POSIX и Windows — этот интерфейс можно использовать, например, для построения виртуальных ФС.
  • Добавлена начальная поддержка HDRI в dlib.image (реализация формата изображений с плавающей запятой в dlib.image.hdri). Кроме того, обеспечена поддержка распараллеливания обработки изображений (dlib.image.parallel), добавлена поддержка чтения форматов TGA и BMP. Чтение/запись графических форматов теперь основаны на потоках, поэтому имеется возможность загружать изображения, например, напрямую из архивов.
  • Элементы матриц (dlib.math.matrix) теперь располагаются по столбцам, а не по строкам. Это серьезно нарушило обратную совместимость, но если вы не используете внутренние данные матриц и пользуетесь только внешним API, то это изменение не должно повлечь никаких проблем.

Более полный чейнджлог, а также исходники релиза вы можете найти на GitHub:
https://github.com/gecko0307/dlib/releases/tag/v0.3.0

Распараллеливание обработки изображений

API dlib.image позволяет создавать фильтры, которые легко распараллеливать на несколько процессоров. Изображение условно разбивается на несколько блоков заданного размера, которые затем обрабатываются фильтром через std.parallelism.

import std.parallelism;
import dlib.functional.range;
import dlib.image.image;

struct Block
{
    uint x1, y1;
    uint x2, y2;
}

alias Range!uint PixRange;

void parallelFilter(
     SuperImage img, 
     void delegate(PixRange blockRow, PixRange blockCol) ffunc, 
     uint bw = 100,
     uint bh = 100)
{
    if (bw > img.width)
        bw = img.width;
    if (bh > img.height)
        bh = img.height;

    uint numBlocksX = img.width / bw + ((img.width % bw) > 0);
    uint numBlocksY = img.height / bh + ((img.height % bh) > 0);

    Block[] blocks = new Block[numBlocksX * numBlocksY];
    foreach(x; 0..numBlocksX)
    foreach(y; 0..numBlocksY)
    {
        uint bx = x * bw;
        uint by = y * bh;

        uint bw1 = bw;
        uint bh1 = bh;

        if ((img.width - bx) < bw)
            bw1 = img.width - bx;
        if ((img.height - by) < bh)
            bh1 = img.height - by;

        blocks[y * numBlocksX + x] = Block(bx, by, bx + bw1, by + bh1);
    }

    foreach(i, ref b; taskPool.parallel(blocks))
    {
        ffunc(range!uint(b.x1, b.x2),
              range!uint(b.y1, b.y2));
    }
}

Пример (закрашивание сплошным цветом):

SuperImage filterTestMultithreaded(SuperImage img)
{
    auto res = img.dup;
    
    img.parallelFilter((PixRange row, PixRange col)
    {
        foreach(x; row)
        foreach(y; col)
        {
            res[x, y] = hsv(180.0f, 1.0f, 0.5f);
        }
    });
    
    return res;
}

Для сравнения — однопоточный вариант:

SuperImage filterTestSinglethreaded(SuperImage img)
{
    auto res = img.dup;
    
    foreach(x; img.row)
    foreach(y; img.col)
    {
        res[x, y] = hsv(180.0f, 1.0f, 0.5f);
    }
   
    return res;
}

На двухъядерном Intel Dual Core T2390 (1.86 ГГц) многопоточный вариант показывает прирост производительности на 70%.

Итоги 2013 года

Завершился 2013 год, в течение которого я всеми силами старался выкроить свободное время для работы над Atrium и сопутствующими инструментами. Подведу итоги: что было сделано, какие в прошедшем году произошли важные релизы и достижения.

  • Сециально для Atrium был разработан игровой физический движок dmech с поддержкой нескольких видов геометрических тел и сочленений. Он еще далек от совершенства, но уже пригоден для использования в простых задачах игровой динамики;
  • Было выпущено 6 номеров электронно-познавательного журнала «FPS» (№№ 22, 23, 24, 25, 26, 27). Кстати, в феврале 2014 года журналу исполняется 6 лет!
  • Состоялось серьезное обновление dlib: в частности, пакетов dlib.math и dlib.image. Библиотека обогатилась новой функциональностью, переехала на GitHub и обзавелась поддержкой DUB;
  • Вышла Cook2, экспериментальная ветка программы сборки проектов Cook со значительными изменениями и улучшениями;
  • Вышла альфа-версия Arrow — тетрисоподобной игры-головоломки с оригинальной механикой.

Огромное спасибо всем, кто так или иначе помогал мне в течение года:

  • Андрею Пенечко (MrSmith33) — за багрепорты и багфиксы в dlib;
  • Наталии Чумаковой (d_o_r_i_a_n_a) — за помощь по матчасти и тестирование всех программ на Windows 7, а также за сотрудничество по журналу;
  • Александру Санникову (Suslik) — за советы и помощь по физике.

    Журнал «FPS» №27

    Вышел 27 номер электронного PDF-журнала «FPS», посвященного разработке игр, программированию, компьютерной графике и звуку.

    Читайте в этом номере:

    > Подборка новостей по Blender
    > Тон Розендаль о будущем интерфейса Blender
    > GIMP: цветокоррекция на Python
    > От мольберта — к дисплею. Заметки о цифровой живописи
    > Физический движок своими руками. Часть IV
    > Математика в dlib
    > Ranges: диапазоны в D
    > Игровые новости из мира Linux
    > Право на творчество

    Номер доступен для онлайн-чтения и загрузки на сервисе Issuu.com, Документах Google и Dropbox.

    Последние новости по проекту вы можете узнать в публичной странице журнала в социальной сети Google+: http://gplus.to/fpsmag. Добавляйте нас в круги, оставляйте свои комментарии и отписывайтесь в нашем сообществе.

    Архив номеров журнала здесь.

    Обновление dlib.image

    В dlib.image появилась возможность отслеживать прогресс во время работы фильтров. Для этого используется многопоточность – необходимо создать класс-враппер, наследующий от FilteringThread. Прогресс (от 0 до 1) считывается из свойства progress для SuperImage. В данном примере показано, как использовать эту функциональность для вывода прогресса свертки в консоль:

    import std.stdio;
    import dlib.image.image;
    import dlib.image.io.png;
    import dlib.image.filters.convolution;
    import dlib.image.fthread;
    
    class ConvolutionThread: FilteringThread
    {
        float[] kernel;
        
        this(SuperImage img, float[] k)
        {
            super(img);
            kernel = k;
        }
        
        override void run()
        {
            output = image.convolve(kernel);
        }
        
        override void onRunning()
        {
            writef("Convolving %s%%", cast(uint)(image.progress * 100));
            write("r");
            stdout.flush();
        }
        
        override void onFinished()
        {
            writeln();
        }
    }
    
    void main()
    {
        auto img = loadPNG("test.png");
        img = (new ConvolutionThread(img, Kernel.Emboss)).filtered;
        img.savePNG("output.png");
    }

    Рефакторинг матриц в dlib

    На днях состоялось грандиозное обновление пакета линейной алгебры dlib.math. Изменения коснулись, главным образом, реализации матриц. Если раньше матрицы 2×2, 3×3 и 4×4 имели каждая отдельную независимую реализацию, то теперь все они являются частными случаями обобщенной квадратной матрицы Matrix!(T,N) (где T – тип элементов, N – размерность). Она содержит все необходимые общие методы для матриц любого размера (нахождение определителя, нахождение обратной матрицы, нахождение матрицы миноров и алгебраических дополнений и т.д.), оптимизированные, где это возможно, для размерностей 2, 3 и 4. Таким образом, нынешние специализации Matrix2x2f, Matrix3x3f и Matrix4x4f практически идентичны их прежним аналогам.

    Новая реализация создана с учетом обратной совместимости, но все-таки есть несколько критичных изменений:

    1. Больше нет шаблонов Matrix2x2!(T), Matrix3x3!(T), Matrix4x4!(T). Используйте вместо них Matrix!(T,2), Matrix!(T,3) и Matrix!(T,4). При этом псевдонимы на специализации типа Matrix2x2f и Matrix4x4d сохранены;

    2. Нет доступа к элементам матриц 4×4 через поля m*, t* и h*. Возможен только доступ через поля a*. Это справедливо для матриц любого размера:

    a11 a12 a13 a14 .. a1N
    a21 a22 a23 a24 .. a2N
    a31 a32 a33 a34 .. a3N
    a41 a42 a43 a44 .. a4N
     :   :   :   :  .
    aN1 aN2 aN3 aN4  ' aNN

    2. Все аффинные преобразования (функции rotationMatrix, translationMatrix и др.) и утилитарные функции для матриц вынесены в отдельный модуль dlib.math.affine. Там же находятся функции right, up, forward, translation, scaling, которые раньше были опрелены как методы в Matrix4x4!(T). Благодаря UFCS, их и теперь можно использовать как методы – однако все они теперь представляют собой свойства только для чтения. Пока они определены только для Matrix!(T,4), но в будущем функции базиса (right, up, forwartd) будут доступны и для Matrix!(T,3).

    3. В целях обратной совместимости сохраняются модули dlib.math.matrix2x2, dlib.math.matrix3x3, dlib.math.matrix4x4, но они помечены как deprecated. Вместо них импортируйте dlib.math.matrix (и dlib.math.affine, если вам нужны аффинные преобразования)

    2. Не рекомендуется использовать identityMatrix3x3!(T) и identityMatrix4x4!(T). Единичные матрицы создаются при помощи статического метода identity: например, Matrix3x3f.identity.

    3. Не рекомендуется трансформировать векторы методом transform. Вместо этого лучше использовать умножение вектора на матрицу: Vector3f(1, 2, 3) * myMatrix.

    4. Любые матрицы можно создавать при помощи функции-фабрики matrixf, которая автоматически определяет размерность на основе входных данных:

    auto m1 = matrixf(
        8, 3, 2, 0,
        4, 0, 2, 0,
        1, 3, 3, 0,
        0, 0, 3, 1
    );

    Это выражение создаст матрицу типа Matrix!(float,4) и присвоит ее переменной m1.

    Убедительная просьба всем пользователям dlib сообщить мне (в Issues в репозитории на GitHub, либо на почту – gecko0307@gmail.com), если будут обнаружены какие-то несостыковки и баги, связанные с данным рефакторингом матриц.