Подтвердить что ты не робот

Как я могу улучшить качество Html5 Canvas?

Я писал небольшой плагин javascript, и у меня возникают небольшие проблемы с улучшением общего качества рендеринга холста. Я искал через Интернет здесь и там, но не могу найти ничего, что имеет смысл.

Линии, созданные из моих кривых, не являются гладкими, если вы посмотрите на jsfiddle ниже, вы поймете, что я имею в виду. Это выглядит неровно. Есть ли способ улучшить качество? Или есть Canvas Framework, который уже использует какой-либо метод для автоматического улучшения его качества, который я могу использовать в своем проекте?

Мой ленточный рендеринг

Не уверен, что это поможет, но я использую этот код в начале моего script:

var c = document.getElementsByClassName("canvas");

  for (i = 0; i < c.length; i++) {
    var canvas = c[i];
    var ctx = canvas.getContext("2d");
    ctx.clearRect(0,0, canvas.width, canvas.height);
    ctx.lineWidth=1;
  }

}

Заранее спасибо

Пример моего кода кривой:

var data = {
diameter: 250,
slant: 20,
height: 290
};

for (i = 0; i < c.length; i++) {
  var canvas = c[i];
  var ctx = canvas.getContext("2d");
  ctx.beginPath();
    ctx.moveTo( 150 + ((data.diameter / 2) + data.slant ), (data.height - 3) );
    ctx.quadraticCurveTo( 150 , (data.height - 15), 150 - ((data.diameter / 2) + data.slant ), (data.height - 3));
    ctx.lineTo( 150 - ((data.diameter / 2) + data.slant ), data.height );
    ctx.quadraticCurveTo( 150 , (data.height + 5), 150 + ((data.diameter / 2) + data.slant ), data.height);
  ctx.closePath();
  ctx.stroke();
}
4b9b3361

Ответ 1

Проблема

Это один из тех случаев, когда практически невозможно получить гладкий результат без ручной настройки его.

Причина связана с минимальным пространством для распределения сглаживающих пикселей. В этом случае мы имеем только одну высоту пикселя между каждой секцией квадратичной кривой.

Если мы посмотрим на кривую без сглаживания, мы можем более четко увидеть это ограничение (без сглаживания каждого пикселя на целочисленной позиции):

no smoothing

Красная строка указывает на один раздел, и мы видим, что переход между предыдущим и следующим разделами должен быть распределен по высоте одного пикселя. См. мой ответ здесь, как это работает.

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

Когда длинная строка переходит от y к y +/- 1 (или x к x +/- 1), между конечными точками, которые приземляются на идеальную границу, не будет ни одного пикселя, что означает, что каждый пиксель между ними представляет собой оттенок.

Если мы рассмотрим несколько сегментов из текущей строки, мы можем более четко видеть оттенки и как это влияет на результат:

closeup

Дополнительно

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

Вам нужно будет пройти код, чтобы удалить ненужные штрихи, чтобы вы получили один штрих в каждом месте. Например, у вас есть несколько closePaths(), которые свяжут конец пути с началом и рисуют двойные строки и т.д.

Комбинация этих двух должна обеспечивать хороший баланс между гладкими и резкими.

Сглаживающий стенд

Эта демонстрация позволяет вам увидеть эффект от распределения сглаживания на основе доступного пространства.

Чем больше изогнута кривая, тем короче каждая секция становится и требует меньшего сглаживания. Результат: плавная линия.

var ctx = c.getContext("2d");
ctx.imageSmoothingEnabled = 
  ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false; // for zoom!

function render() {
  ctx.clearRect(0, 0, c.width, c.height);
  !!t.checked ? ctx.setTransform(1,0,0,1,0.5,0.5):ctx.setTransform(1,0,0,1,0,0);
  ctx.beginPath();
  ctx.moveTo(0,1);
  ctx.quadraticCurveTo(150, +v.value, 300, 1);
  ctx.lineWidth = +lw.value;
  ctx.strokeStyle = "hsl(0,0%," + l.value + "%)";
  ctx.stroke();
  vv.innerHTML = v.value;
  lv.innerHTML = l.value;
  lwv.innerHTML = lw.value;
  ctx.drawImage(c, 0, 0, 300, 300, 304, 0, 1200, 1200);  // zoom
}

render();
v.oninput=v.onchange=l.oninput=l.onchange=t.onchange=lw.oninput=render;
html, body {margin:0;font:12px sans-serif}; #c {margin-top:5px}
<label>Bend: <input id=v type=range min=1 max=290 value=1></label>
<span id=vv></span><br>
<label>Lightness: <input id=l type=range min=0 max=60 value=0></label>
<span id=lv></span><br>
<label>Translate 1/2 pixel: <input id=t type=checkbox></label><br>
<label>Line width: <input id=lw type=range min=0.25 max=2 step=0.25 value=1></label>
<span id=lwv></span><br>
<canvas id=c width=580></canvas>

Ответ 2

Этот вопрос показался мне немного странным. Хотя рендеринг холста, хотя и не самый лучший по сравнению с рендерингом высокого класса, по-прежнему очень хорош. Так почему же возникает такая проблема с этим примером. Я собирался оставить его, но 500 очков стоит другого взгляда. Из этого я могу дать два совета, решение и альтернативу.

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

Во-вторых, всегда пишите аккуратный код с комментариями. Здесь есть 4 ответа, и никто не выбрал этот недостаток. Это потому, что представленный код довольно грязный и трудно понять. Я угадываю (почти как я), другие вообще пропустили ваш код, а не выяснили, что он делает неправильно.

Обратите внимание

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

Результаты исследования проблемы в порядке качества (на мой взгляд)

Генетический алгоритм

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

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

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

Результат поиска GA.

Лучший результат

см. примечание 1 для шагов обработки

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

  • Сглаживание неравномерно. Там, где вы обычно ожидаете равномерного изменения интенсивности, этот метод создает ступенчатый градиент (почему это делает его лучше, я не знаю).
  • Темные узлы. Просто, когда переход от одной строки к следующей почти завершен, линия ниже или выше отображается заметно темнее для нескольких пикселей, а затем возвращается к светлому оттенку, когда линия подходит к строке. Это швы, чтобы компенсировать осветление общей линии, поскольку она разделяет ее внутренность в двух строках.

введите описание изображения здесь

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

Примечание 1 Методы, используемые для визуализации вышеуказанного изображения. Размер холста на экране размером 1200 на 1200. Отображается в масштабе 4 ctx.setTransform(4,0,0,4,0,0), смещение пикселя y 3 ctx.translate(0,3). Ширина линии 0.9pixels отображается дважды на белом фоне, размытие счетчика 1 пикселя фотона повторяется 3 раза (аналогично свертке 3 * 3 гауссова размытие, но немного меньший вес по диагоналям), 4 раза вниз по образцам через 2-х ступенчатый снимок с использованием средств подсчета фотонов (каждый пиксельный канал является квадратным корнем из среднего квадрата из 4 (2 на 2) дискретизированных пикселей). Заострите один проход (к сожалению, это очень сложный пользовательский фильтр заточки (например, некоторые фильтры для заточки пин/пикселов)), а затем один раз накладывается на ctx.globalCompositeOperation = "multiply" и ctx.globalAlpha = 0.433, затем фиксируется на холсте. Вся обработка, выполненная в Firefox

Исправление кода

Ужасный результат рендеринга был фактически вызван некоторыми незначительными несоответствиями рендеринга в вашем коде.

Ниже приведено до и после исправления кода. Как видите, наблюдается заметное улучшение.

Befor и после исправления кода.

Итак, что вы сделали неправильно?

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

Вставьте свой код только при визуализации формы. Комментарии показывают изменения.

ctx.beginPath();
// removed the top and bottom lines of the rectangle
ctx.moveTo(150, 0);
ctx.lineTo(150, 75);
ctx.moveTo(153, 0);
ctx.lineTo(153, 75);
// dont need close path
ctx.stroke();
ctx.beginPath();
ctx.moveTo((150 - (data.diameter / 2)), 80);
ctx.quadraticCurveTo(150, 70, 150 + (data.diameter / 2), 80);
ctx.lineTo(150 + (data.diameter / 2), 83);
ctx.quadraticCurveTo(150, 73, 150 - (data.diameter / 2), 83);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
// removed the two quadratic curves that where drawing over the top of existing ones
ctx.moveTo(150 + (data.diameter / 2), 83);
ctx.lineTo(150 + ((data.diameter / 2) + data.slant), data.height);
ctx.moveTo(150 - ((data.diameter / 2) + data.slant), data.height);
ctx.lineTo(150 - (data.diameter / 2), 83);
// dont need close path
ctx.stroke();

ctx.beginPath();
// removed a curve
ctx.moveTo(150 + ((data.diameter / 2) + data.slant), (data.height - 3));
ctx.quadraticCurveTo(150, (data.height - 15), 150 - ((data.diameter / 2) + data.slant), (data.height - 3));
// dont need close path
ctx.stroke();

ctx.beginPath();
ctx.moveTo(150 + ((data.diameter / 2) + data.slant), data.height);
ctx.quadraticCurveTo(150, (data.height - 10), 150 - ((data.diameter / 2) + data.slant), data.height);
ctx.quadraticCurveTo(150, (data.height + 5), 150 + ((data.diameter / 2) + data.slant), data.height);
ctx.closePath();
ctx.stroke();

Итак, теперь рендеринг намного лучше.

Субъективный глаз

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

ОТСУТСТВУЮЩИЙ ОТБОР

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

Существует много методов выборки вниз, но многие из них не имеют практического применения из-за времени, затрачиваемого на обработку изображения.

Самая быстрая выборка вниз может быть выполнена с помощью графических процессоров и собственных обработок рендеринга. Просто создайте внеэкранный холст с разрешением 2 раза или 4 раза больше, чем требуется, затем используйте преобразование для масштабирования рендеринга изображения (так что вам не нужно менять код рендеринга). Затем вы визуализируете это изображение с требуемым разрешением для результата.

Пример понижающей дискретизации с использованием 2D API и JS

var canvas = document.getElementById("myCanvas"); // get onscreen canvas
// set up the up scales offscreen canvas
var display = {};
display.width = canvas.width;
display.height = canvas.height;
var downSampleSize = 2;
var canvasUp = document.createElement("canvas");
canvasUp.width = display.width * downSampleSize;
canvasUp.height = display.height * downSampleSize;
var ctx = canvasUp.getContext("2D");
ctx.setTransform(downSampleSize,0,0,downSampleSize,0,0);

// call the render function and render to the offscreen canvas

Once you have the image just render it to you onscreen canvas

ctx = canvas.getContext("2d");
ctx.drawImage(canvasUp,0,0,canvas.width,canvas.height);

На следующих изображениях показан результат сэмплирования 4 * вниз и изменение ширины линии от 1,2 до 0,9 пикселя (обратите внимание, что ширина линии с расширенной выборкой составляет 4 *, что 4.8, 4.4, 4 и 3.6)

введите описание изображения здесь введите описание изображения здесь

Следующее изображение 4 * вниз с использованием повторная выборка Lanczos достаточно быстрая перевыборка (лучше подходит для изображений)

введите описание изображения здесь

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

Вниз выборка также позволяет значительно более тонкое управление шириной линии (видимой). рендеринг при разрешении экрана дает слабые результаты при изменении ширины полосы в 1/4 пикселя. Используя понижающую дискретизацию, вы удваиваете и увеличиваете четность в 1/8 и 1/16-й мелкой детали (помните, что при рендеринге в разрешении субпикселей есть другие типы эффектов сглаживания)

Динамический диапазон

Динамический диапазон в цифровых носителях относится к диапазону значений, которые могут обрабатывать носители. Для холста, диапазон которого составляет 256 (8 бит) на цветной канал. Человеческому глазу трудно найти разницу между параллельными значениями, скажем, 128 и 129, поэтому этот диапазон почти повсеместен в области компьютерной графики. Современный графический процессор, хотя и может отображать на гораздо более высоких динамических диапазонах 16 бит, 24 бит, 32 бит на канал и даже двойную точность поплавков 64 бит.

Соответствующий 8-битный диапазон хорош для 95% случаев, но страдает, когда отображаемое изображение принудительно переходит в более низкий динамический диапазон. Это происходит, когда вы создаете линию поверх цвета, близкого к цвету линии. В questio изображение отображается на не очень ярком фоне (пример # 888), результатом является то, что сглаживание имеет только 7 бит, уменьшающих вдвое динамический диапазон. Проблема усугубляется тем фактом, что если изображение отображается на прозрачном фоне, где сглаживание достигается путем изменения альфа-канала, что приводит к введению второго уровня артефактов.

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

Ниже приведен пример рендеринга для различных интенсивностей фона, они визуализируются с использованием 2 * вниз выборки на предварительно представленном фоне. BG обозначает интенсивность фона.

Сравнение интенсивности фона на результат рендеринга. Обратите внимание, что это изображение должно слишком сильно соответствовать странице и ниспровергается с помощью браузера, тем самым добавляя дополнительные артефакты.

ИСТИННЫЙ ТИП типа

Пока есть другой метод. Если вы рассматриваете экран, состоящий из пикселей, каждый пиксель имеет 3 части красного, зеленого, синего, и мы группируем их всегда, начиная с красного.

Но неважно, где начинается пиксель, все, что имеет значение, состоит в том, что пиксель имеет 3 цвета rgb, это может быть gbr или brg. Когда вы смотрите на такой экран, вы фактически получаете 3-кратное горизонтальное разрешение относительно краев пикселей. Размер пикселя остается тем же, но смещается. Так Microsoft делает свой специальный шрифт (true type). К сожалению, у Microsoft много патентов на использование этого метода, поэтому все, что я могу сделать, это показать вам, как выглядит рендеринг, игнорируя границы пикселей.

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

пример истинного типа

Ответ 3

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

Решение состоит в том, чтобы иметь эффект размытия вокруг линий и сделать линию более гладкой.

Для этого вам нужно использовать свойства shadowColor, shadowBlur, lineCap и lineJoin контекста canvas.

Поставьте следующую настройку и попробуйте рисовать строки.

 for (i = 0; i < c.length; i++) {
    var canvas = c[i];
    var ctx = canvas.getContext("2d");

    ctx.shadowColor = "rgba(0,0,0,1)";
    ctx.shadowBlur = 2;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.lineWidth = 1;
    ctx.strokeStyle = 'black';

    ctx.clearRect(0,0, canvas.width, canvas.height);

  }

Вот результат

result

Попробуйте сыграть с прозрачностью shadowColor и размером размытия вместе с шириной и цветом линии. Вы можете получить потрясающие результаты.

На стороне примечания, ваш проект звучит больше SVG для меня, чем Canvas. Вероятно, вам стоит подумать о переходе на SVG, чтобы улучшить поддержку чертежа и производительность.

Обновление

Вот тонкая настройка

ctx.shadowColor = "rgba(128,128,128,.2)";
ctx.shadowBlur = 1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 1;
ctx.strokeStyle = 'gray';

введите описание изображения здесь

Ответ 4

Извините, что опоздал на вечеринку, но все ответы здесь слишком сложны.

На самом деле вы видите отсутствие гамма-коррекции. Посмотрите на примеры Antialias 1 & 2 здесь: http://bourt.com/2014/ (сначала вам нужно будет откалибровать значение гаммы для вашего монитора), и это краткое объяснение: https://medium.com/@alexbourt/использование гамма-везде-da027d9dc82f

Векторы нарисованы как будто в линейном цветовом пространстве, в то время как пиксели существуют в пространстве с гамма-коррекцией. Это так просто. К сожалению, у Canvas нет поддержки гаммы, так что вы застряли.

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

(Поскольку этот аргумент неизменно возникает, я сейчас его рассмотрю: лучше ошибиться на стороне гаммы, чем нет. Если вы скажете: "Ну, я не знаю, какой будет гамма пользовательского монитора", и оставьте его на уровне 1.0 результат будет НЕПРАВИЛЬНЫМ почти во всех случаях. Но если вы сделаете обоснованное предположение, скажем, 1.8, то для значительного процента пользователей вы догадались, что-то близкое к тому, что подходит для их монитора.)

Ответ 5

Одной из причин появления линий размытия является рисование промежуточных пикселей. Этот ответ дает хороший обзор системы координат холста:

fooobar.com/questions/252240/...

Один из способов сохранения целочисленных координат, но все же получение четких строк - это преобразование контекста на 0,5 пиксела:

context.translate(0.5,0.5);

Взгляните на фрагмент ниже. Второй холст переводится на (0,5, 0,5), делая линию, нарисованную целыми координатами, выглядят четкими.

Это должно привести к тому, что ваши прямые линии будут исправлены. Кривые, диагональные линии и т.д. Будут сглаживаться (серые пиксели вокруг штрихов). Не так много вы можете с этим поделать. Чем выше разрешение, тем они менее заметны, и все линии, за исключением прямых, выглядят лучше сглаженными в любом случае.

function draw(ctx){
    ctx.beginPath();
    ctx.moveTo(25, 30);
    ctx.lineTo(75, 30);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(25, 50.5);
    ctx.lineTo(75, 50.5);
    ctx.stroke();
}

draw(document.getElementById("c1").getContext("2d"))

var ctx = document.getElementById("c2").getContext("2d");
ctx.translate(0.5, 0.5);
draw(ctx);
    <canvas id="c1" width="100" height="100" style="width: 100px; height: 100px"></canvas>
    <canvas id="c2" width="100" height="100" style="width: 100px; height: 100px"></canvas>

Ответ 6

Сглаживание помогает много. Но когда у вас есть угловые линии, близкие к горизонтали, или вертикальные, или мягко изогнутые, сглаживание будет намного более заметным. Особенно для тонких линий шириной менее пары пикселей или около того.

Как указывает Maciej, если у вас есть линия шириной около 1px и она проходит непосредственно между двумя пикселями, сглаживание приведет к тому, что две пиксели ширины и полусерая.

Возможно, вам придется просто научиться жить с ним. Существует только так много, что можно сделать сглаживание.