Создание Paint Dot Net плагина на C#/CodeLab для ЧБ дизеринга Jarvis Judice Ninke

Если вы фанат такой нелепой программы как Paint net, где отсутствуют бинды на горячие клавиши и прочие удобства современных редакторов, то эта статья для вас! Здесь вы узнаете как бесплатно создавать плагины с помощью CodeLab на языке C# и подготовить их к релизу.

alt

будем делать примерно такой эффект

Что за плагин делаем

Покажу как сделать монохромный дизеринг Jarvis Judice Ninke с гамма-коррекцией и настройками контраста. Плагин будет подготовлен для установки во все программы совместимые с Paint Dot Net плагинами. Вам не потребуется изучать Paint Dot Net API, всё за вас уже сделано в CodeLab и в нём есть автоподстановка. У эффекта будет интерфейс с достаточным количеством настроек, чтобы выполнить цвето-коррекцию изображения и подогнать настройки дизеринга по вкусу и красоте.

alt

в результате гайда получите это

Что надо скачать

  • Скачайте Paint Dot Net или обновите уже установленный, вам потребуется последняя версия.

  • Для работы Paint Dot Net нужен Microsoft .NET 9

  • Скачайте CodeLab и установите его.

Начало работы

Если вы осилили установку CodeLab, то во вкладке эффекты появится вкладка Advanced и там вы найдёте CodeLab, он позволяет писать коды плагинов прямо внутри запущенного окна Paint Dot Net и вы сможете видеть как ваш эффект влияет на картинку в реалтайме, так же там есть редактор интерфейса для вашего эффекта. Мне лично не нравится редактор текста в CodeLab, поэтому я юзаю VS Code и копирую из него оттуда код в CodeLab.

alt

Что нужно жать, чтобы открыть CodeLab

CodeLab берёт на себя большую часть работы с плагином и позволяет вам сосредоточиться именно на написании функционала самого эффекта, вы получаете в распоряжение данные с вашего интерфейса и редактируемые изображения. Эффект будет применяться в пределах выделенной области и в многопотоке (если это позволяет сам алгоритм эффекта, иначе это можно выключить).

Код плагина будет на C#, поэтому вам его неплохо бы знать, а если вы кодили на C++, то короч сишарп это такой искаверканный цпп с гарбадж коллектором, бойлерплейтом из паблик/приват сетер/гетеров в классах и кастами в стиле Си с большим фреймворком Net, который надо докачивать отдельно.

Если шарите за быстрый код обработки 2D картинок

Создавайте плагин так new → classic effect → advanced и пишите unsafe перед описанием функций — это позволит работать с сырыми поинтерами на пиксели картинки внутри таких функций (см. встроенные примеры, которые сами сгенерируются). Любая ошибка в unsafe коде взорвёт ваш плагин и CodeLab, а пейнт вам вежливо напишет лог и предложит перезапуститься, так что следите за выходом за границы буфера. Вроде как это всё компилируется в .dll, не знаю с каким оптимизациями, но наверное SIMD’ы будут, нарезка картинки на слайсы и их многопоточная обработка точно присутствует. Так же есть шаблон кода для обработки на GPU. В целом даже вёдра на core 2 duo тянут обработку classic эффектов с сейв-индексами битмапа, так что можно не париться.

Начало создания плагина

Нажмите File -> New -> Classic Effect чтобы начать писать свой плагин. Появится окно для редактирования кода, его вы можете перетащить в правую часть экрана чтобы видеть и код и вашу тестовую картинку в пейте чтобы по ней определять работоспособность плагина.

alt

Куда жать чтобы создать эффект

Код начинается с секции описания, это просто комменты на C# — CodeLab будет их использовать при подготовке плагина к релизу и потом все кто скачает ваш эффект смогут увидеть инфу об авторе, описание эффекта, версию и т.д. Заполнить эти данные можно и при экспорте плагина.

// Name: название эффекта в меню
// Submenu: название пункта меню в Paint Dot Net, в котором будет эффект
// Author: имя автора
// Title: название окна с эффектом
// Version: 1.0 (меняйте версию, если ваш эффект сильно изменился например)
// Desc: что делает эффект
// Keywords: теги, нужны для форумов по пейнт нэту, разделять их надо через символ |
// URL: https://www.youtube.com/watch?v=xvFZjo5PgG0
// Help: более подробное описание эффекта

Интерфейс плагина

Все кнопки в окне эффекта расставляются автоматически через CodeLab, вы лишь можете выбирать какой элемент интерфейса вы хотите вставить в окно, какое у него значение по умолчанию и как будет называться ваша переменная в коде в которую попадёт значение с интерфейса. Если знаете названия элементов, то можете копипастить их в код вручную.

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

// Всё в секции ниже воспринимается CodeLab как настройки интерфейса:
#region UICode
DoubleSliderControl g_gamma = 1; // [0,3] гамма
DoubleSliderControl g_contrast = 1.0; // [0,3] контраст
DoubleSliderControl g_brightness = 0; // [-1,1] яркость
#endregion

Чтобы посмотреть как будет выглядеть окно вашего эффекта, нажмите Ctrl+P или кнопку со стрелкой запуска (Play).

alt

как выглядит интерфейс эффекта

Дизеринг JJN

Будем использовать дизеринг Jarvis Judice Ninke, он позволяет генерировать специальные узоры на картинке, которые при рассмотрении издалека будут давать результат похожий на плавный градиент как в оригинальной картинке, но при этом будет использоваться палитра из малого числа цветов.

alt

изображение до обработки

alt

изображение после обработки нашим эффектом

Если вам не нравится JJN и вы хотите Аткинсона, Флойда или Байер-дизеринг, смотрите ссылки в конце статьи, там много других алгоритмов.

Попробуем сначала написать простой эффект, который заполняет нашу картинку зелёным цветом, если всё заработает, то значит CodeLab работает верно.

// Name: 1-bit dithering JJN
// Submenu: Color
// Author:
// Title: 1-bit dithering Jarvis-Judice-Ninke
// Version: 1.0
// Desc: монохромный дизеринг по алгоритму Jarvis-Judice-Ninke
// Keywords: 1-bit|dithering|Jarvis-Judice-Ninke|monochrome
// URL:
// Help:
#region UICode
DoubleSliderControl g_gamma = 1; // [0,3] гамма
DoubleSliderControl g_contrast = 1.0; // [0,3] контраст
DoubleSliderControl g_brightness = 0; // [-1,1] яркость
#endregion

/* Эта функция сгенерирована CodeLab'ом и обработка
пикселей в ней выполняется в однопоточном режиме*/
void PreRender(Surface dst, Surface src) {
  // цвет заливки
  var color = new ColorBgra();
  /* порядок байт цвета AARRGGBB: A - Alpha, R - Red, G - green, B - Blue
  0xFF007F00 - тёмно-зелёный непрозрачный цвет */
  color.Bgra = 0xFF007F00;

  /* обход пикселей картинки начинается по Y, это позволяет
  быстрее обрабатывать пиксели, так как в памяти они лежат
  последовательно - индекс пикселя берётся так: (y * src.Width + x)
  Если цикл начинать с X, то будет медленнее */
  for (int y = 0; y < src.Height; ++y) {
    /* Если IsCancelRequested == true, то значит эффект пытаются отменить;
    проверять этот флаг можно каждую строчку картинки, это позволит выключить
    эффект быстрее даже при больших картинках и при этом проверка не вызовет
    тормозов */
    if (IsCancelRequested) return;

    // теперь обход пикселей по ширине (оси X)
    for (int x = 0; x < src.Width; ++x)
      dst[x, y] = color; // закрашиваем все пиксели в цвет 
  }
}

/* Эта функция сгенерирована CodeLab'ом и обработка
пикселей в ней выполняется в многопоточном режиме, её
мы не будем использовать, так как алгоритм дизеринга
Jarvis Judice Ninke хорошо работает в однопотоке*/
void Render(Surface dst, Surface src, Rectangle rect) {
  // сюда ничё не пишем
}

Чтобы двигаться дальше, проверьте что эффект работает, запустите его и посмотрите на редактируемое изображение, если оно зелёное, значит эффект применяется. Все ошибки в коде CodeLab показывает снизу.

alt

куда жать чтоб эффект запустился и что должно быть на экране

Гамма-коррекция sRGB

Гамма-коррекция имеет большое значение в 1-бит дизере (см. скрины ниже). Используйте преобразования линейного цвета в sRGB и обратно, это позволит делать реалистичные градиенты с учётом особенностей монитора.

alt

Обычный линейный градиент и градиент с гамма-коррекцией 2.2. Согласитесь, нижний вариант на глаз больше похож на действительно линейный градиент

alt

Как выглядит градиент после дизеринга

Основной код плагина:

// Name: 1-bit dithering JJN
// Submenu: Color
// Author:
// Title: 1-bit dithering Jarvis-Judice-Ninke
// Version: 1.0
// Desc: монохромный дизеринг по алгоритму Jarvis-Judice-Ninke
// Keywords: 1-bit|dithering|Jarvis-Judice-Ninke|monochome
// URL:
// Help:

#region UICode
DoubleSliderControl g_gamma = 1; // [0,3] гамма
DoubleSliderControl g_contrast = 1.0; // [0,3] контраст
DoubleSliderControl g_brightness = 0; // [-1,1] яркость
#endregion

// загоняет значения val в пределы между min и max
double clamp(double val, double min, double max) {
  if (val < min)
    val = min;
  if (val > max)
    val = max;
  return val;
}

// из sRGB компонента в линейный
double to_linear_space(double srgb_color) {
  if (srgb_color <= 0.04045)
    return srgb_color / 12.02;
  return Math.Pow(((srgb_color + 0.055) / 1.055), 2.4);
}

// из линейного компонента в sRGB
double to_srgb_space(double linear_color) {
  if (linear_color <= 0.0031308)
    return 12.92 * linear_color;
  return (1.055 * Math.Pow(linear_color, 1.0 / 2.4)) - 0.055;
}

// преобразование в серый оттенок по стандарту bt.2001
double desaturate(double r, double g, double b) {
  return r * 0.2627 + g * 0.6780 + b * 0.0593;
}

// изменить якрость
double brightness(double src, double value) {
  return src + value;
}

// изменить гамму
double gamma(double src, double value) {
  return Math.Pow(src, 1.0 / value);
}

// изменить контраст
double contrast(double src, double value) {
  return (src - 0.5) * value + 0.5;
}

// сделать цвет монохромным
double threshold(double src) {
  return src >= 0.5 ? 1.0 : 0.0;
}

// BGRA цвет преобразовать в серый оттенок
double bgra_to_gray(ColorBgra src) {
  var r = to_linear_space(src.R / 255.0);
  var g = to_linear_space(src.G / 255.0);
  var b = to_linear_space(src.B / 255.0);
  return desaturate(r, g, b);
}

// серый оттенок преобразовать в BGRA цвет 
ColorBgra gray_to_bgra(double src) {
  // обратная гамма-коррекция
  src = to_srgb_space(src);
  var ret = new ColorBgra();
  var luma = (byte)(clamp(src, 0.0, 1.0) * 255.0);
  ret.B = luma;
  ret.G = luma;
  ret.R = luma;
  ret.A = 0xFF;
  return ret;
}

// обработка оттенка
byte color_correction(byte src) {
  // нормализация байта (0..255 -> 0..1)
  var src_double = ((double)src) / 255.0;
  src_double = gamma(src_double, g_gamma);
  src_double = brightness(src_double, g_brightness);
  src_double = contrast(src_double, g_contrast);
  // преобразовать оттенок обратно в байт
  return (byte)(clamp(src_double, 0, 1) * 255.0);
}

// прибавить ошибку дизеринга к другому пикселю изображения
void add_error(Surface dst, int x, int y, double error) {
  // проверка на выход координат пикселя за границы изображения
  if (x < 0) return;
  if (x >= dst.Width) return;
  if (y < 0) return;
  if (y >= dst.Height) return;

  var gray = bgra_to_gray(dst[x, y]);
  gray += error;
  dst[x, y] = gray_to_bgra(gray);
}

// дизеринг Jarvis Judice Ninke:
void PreRender(Surface dst, Surface src) {
  /* пиксели изменяются на уже отрисованной картинке,
  поэтому оригинал копируем на редактируемую  */
  for (int y = 0; y < dst.Height; y++) {
    if (IsCancelRequested) return;

    for (int x = 0; x < dst.Width; x++) {
      var color = src[x, y];
      // применить цветокор к картинке перед редактированием:
      color.B = color_correction(color.B);
      color.G = color_correction(color.G);
      color.R = color_correction(color.R);
      dst[x, y] = color;
    }
  }

  for (int y = 0; y < dst.Height; y++) {
    if (IsCancelRequested) return;

    for (int x = 0; x < dst.Width; x++) {
      // обесцветить текущий пиксель
      var old_pixel = dst[x, y];
      var old_gray = bgra_to_gray(old_pixel);
      // преобразовать в монохром
      var binary_color = threshold(old_gray);
      // вернуть пиксель обратно, но уже обработанным
      var new_pixel = gray_to_bgra(binary_color);
      dst[x, y] = new_pixel;
      // вычислить "ошибку" дизеринга и распространить её на соседние пиксели:
      double error = old_gray - binary_color;
      add_error(dst, x+1, y+0, error * (7.0/48.0));
      add_error(dst, x+2, y+0, error * (5.0/48.0));
      add_error(dst, x-2, y+1, error * (3.0/48.0));
      add_error(dst, x-1, y+1, error * (5.0/48.0));
      add_error(dst, x+0, y+1, error * (7.0/48.0));
      add_error(dst, x+1, y+1, error * (5.0/48.0));
      add_error(dst, x+2, y+1, error * (3.0/48.0));
      add_error(dst, x-2, y+2, error * (1.0/48.0));
      add_error(dst, x-1, y+2, error * (3.0/48.0));
      add_error(dst, x+0, y+2, error * (5.0/48.0));
      add_error(dst, x+1, y+2, error * (3.0/48.0));
      add_error(dst, x+2, y+2, error * (1.0/48.0));
    }
  }
}

void Render(Surface dst, Surface src, Rectangle rect) {}

В общем это всё, если не работает, спрашивайте в комментах к статье, может помогу.

Как это всё собрать?

Нарисуйте иконку эффекта в .PNG файле размером 16×16, она будет отображаться при выборе в меню Paint Net (это не обязательно). Чтобы сгенерировать файлы для установки вашего эффекта, нажмите Build DLL (Ctrl+B). В результате у вас будет на рабочем столе архив с вашим плагином и .bat скриптом для установки — скрипт сам ищет установленный у пользователя Пейнт Нэт и закидывает эффект куда надо (работает от имени админа). Если же юзеры будут кряхтеть на ваш батник и думать что в нём вирусы, можете дать им инструкцию самостоятельно копипастить .dll файл эффекта в папку путь к\Paint-dot-net\Effects\. При перезагрузке пейнта вы увидите эффект в том меню, в котором вы настроили при билде или в заголовке файла с кодом плагина.

alt

Чего ожидаем увидеть в релизном архиве

Всё готово, можете заливать в сеть, кидать друзьям или на форум (Русский, Английский)

Заключение

Если хотите универсальные плагины, используйте G’Mic. Плагины G’Mic работают в Krita и в Paint Net, если вы накатите специальный плагин для запуска G’Mic. Я написал эту статью, потому что мне было не лень и появилось вдохновение из-за того что я узнал что неправильно обрабатывал гамму раньше и что sRGB решает. Сам же я пишу более сложные эффекты, можете посмотреть и скачать их тут. Некоторые программы совместимы с плагинами Paint Dot Net и вы можете закинуть в них свой .dll файл эффекта без изменений.

Ссылки

  • Ditherista — Бесплатная программа с GUI, в которой собрано большое количество дизерингов

  • DHALF — древний .txt гайд по многим алгоритмам дизеринга

  • Гайд от Bisqwit’а по цветным дизерингам и влиянии разных цветовых систем на них

  • Мои плагины, среди них даже есть симулятор танчиков с денди, который работает прямо в пейнте

© Habrahabr.ru