Сглаживание битовых изображений
Эта статья является логическим продолжением предыдущей статьи, описывающей метод кодирования пиктограмм для кнопок и панелей инструментов в виде строк, а также их декодирования обратно в изображения.
Битовые картинки имеют только два цвета — цвет фона, обычно, прозрачный, он задаётся в алгоритме декодера, и основной видимый цвет, который, как правило, передаётся в функцию декодера. Битовые изображения легко рисовать в редакторе, но главный недостаток — отсутствие плавности и смягчающих градиентов на изгибах линий, дуг, окружностей, из-за чего изображения могут смотреться немного грубо и угловато, становится видна их пиксельная структура.
Один метод — избегать изогнутых линий и контуров в пиктограммах при их создании в редакторе. Другой, более интересный вариант — применить процедурное сглаживание изображений, также известное как antialiasing. Далее в качестве примера приводится один из простых вариантов сглаживания, применяемый мной в разных проектах.
Рассмотрим базовый код на C# для декодирования изображения из строки:
// decoder
Bitmap stringToBitmap(Color pen, string s)
{
byte basecode = s[0];
byte w = s[1] - basecode;
byte h = s[2] - basecode;
Bitmap bmp = new Bitmap(w, h);
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
bmp.SetPixel(x, y, ((s[3 + (y * w + x) / 6] - basecode) & (1 << (y * w + x) % 6)) > 0 ? pen : Color.Transparent);
return bmp;
}
Этот код попиксельно восстанавливает изображение из кода строки s. Задавая всем пикселям, чей бит в строке равен 1 значение параметра pen, остальным же Color.Transparent.
Теперь добавим простое внешнее сглаживание — для пустых пикселей, что находятся возле двух или трёх закрашенных соседей декодированного изображения, будет добавляться тот же цвет, но на ¾ более прозрачный. То есть просто будем использовать тот же цвет pen, но с pen.alpha / 4. Соседями считаются только пиксели сверху, снизу, справа и слева. Коэффициент в знаменателе не обязательно должен быть равен 4, можно поэкспериментировать с ним. Мне больше понравился такой вариант.
В следующем примере кода декодируем строку s указанным выше алгоритмом в предварительный массив, затем добавляем сглаживание и заполняем изображение.
// decoder with antialiasing
Bitmap stringToBitmap(Color pen, string s)
{
Color ppart = Color.FromArgb(pen.A / 4, pen.R, pen.G, pen.B);
int basecode = s[0];
int w = s[1] - basecode;
int h = s[2] - basecode;
var data = new Color[h, w];
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
data[y, x] = ((s[3 + (y * w + x) / 6] - basecode) & (1 << (y * w + x) % 6)) > 0 ? pen : Color.Transparent;
// antialiasing
Bitmap bmp = new Bitmap(w, h);
for (int y = 1; y < h - 1; y++)
for (int x = 1; x < w - 1; x++)
{
int q = 0;
if (data[y - 1, x] == pen) q++;
if (data[y + 1, x] == pen) q++;
if (data[y, x - 1] == pen) q++;
if (data[y, x + 1] == pen) q++;
if ((q == 2 || q == 3) && data[y, x] == Color.Transparent) data[y, x] = ppart;
bmp.SetPixel(x, y, data[y, x]);
}
return bmp;
}
Несколько примеров битовых изображений со сглаживанием и без:
панель инструментов без сглаживания
та же панель, но со сглаживанием
без сглаживания
со сглаживанием
(примечание: если изображение «без сглаживания» выглядит размытым, попробуйте кликнуть по нему правой кнопкой и «открыть изображение в новой вкладке», также может мешать масштаб отображения страницы в браузере, отличный от 100%)
Этот метод может делать кривые линии не только более плавными и гладкими, но и чуть более плотными, за счёт добавления внешних, по отношению к изгибам, пикселей с промежуточным значением цвета. На мой взгляд это даже улучшает вид тонких линий для небольших пиктограмм.
Хотя, иногда, для отдельно взятых изображений, может больше подойти алгоритм без сглаживания, например, когда там есть пиксельные сетки, или мелкие части, которые не хотелось бы замыливать сглаживанием. В таком случае можно сделать универсальный код:
// decoder with antialiasing option
Bitmap stringToBitmap(Color pen, string s, bool antialiasing)
{
Color ppart = Color.FromArgb(pen.A / 4, pen.R, pen.G, pen.B);
int basecode = s[0];
int w = s[1] - basecode;
int h = s[2] - basecode;
var data = new Color[h, w];
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
data[y, x] = ((s[3 + (y * w + x) / 6] - basecode) & (1 << (y * w + x) % 6)) > 0 ? pen : Color.Transparent;
// antialiasing and image fill
Bitmap bmp = new Bitmap(w, h);
for (int y = 1; y < h - 1; y++)
for (int x = 1; x < w - 1; x++)
{
if (antialiasing)
{
int q = 0;
if (data[y - 1, x] == pen) q++;
if (data[y + 1, x] == pen) q++;
if (data[y, x - 1] == pen) q++;
if (data[y, x + 1] == pen) q++;
if ((q == 2 || q == 3) && data[y, x] == Color.Transparent) data[y, x] = ppart;
}
bmp.SetPixel(x, y, data[y, x]);
}
return bmp;
}
Безусловно, такой способ не даёт идеального сглаживания, но не стоит забывать, что мы работаем не с проецированием векторов на сетку изображения, а с битовой картой, и идеально гладкие кривые линии тут вряд ли удастся получить. Хотя, может у читателей есть свои, более интересные способы?
Для рисования битовых изображений использовался BitImageTool, описанный в прошлой статье.
Скачать BitImageTool можно на гитхабе