Как улучшить сглаживание в CanvasRenderingContext2D в Firefox?

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

Вот увеличенное изображение, созданное в Firefox:

Изображение очень четкое, но мы видим неровные края (особенно днище космического корабля, лобовое стекло, носовое крыло).

И в Хроме:

Изображение остается четким (иллюминаторы остаются четкими, все линии) и у нас нет зазубренных краев. Только облака немного размылись.

А в Chrome с отключенным сглаживанием:

Я попытался установить для свойства imageSmoothingEnabled значение true, но в Firefox это не действует, мой пример:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv = "Content-Type" content = "text/html;charset=UTF-8">
</head>
<body>
    <!-- <canvas id = "canvas1" width = "1280" height = "720" style = "width: 640px; height: 360px;"></canvas> -->
    <canvas id = "canvas1" width = "640" height = "360" style = "width: 640px; height: 360px;"></canvas>
    <script>
        const canvas = document.getElementById("canvas1")
        const ctx = canvas.getContext("2d")

        console.info("canvas size", canvas.width, canvas.height);

        const img = new Image()

        img.onload = () => {
            const smooth = true;
            ctx.mozImageSmoothingEnabled = smooth;
            ctx.webkitImageSmoothingEnabled = smooth;
            ctx.msImageSmoothingEnabled = smooth;
            ctx.imageSmoothingEnabled = smooth;
            // ctx.filter = 'blur(1px)';
            ctx.drawImage(img, 0, 0, 3840, 2160, 0, 0, canvas.width, canvas.height);
        }

        img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f8/BFR_at_stage_separation_2-2018.jpg";
    </script>
</body>
</html>

Как применить сглаживание?

Обновлено: сглаживание применяется при просмотре сайта в Chrome, но не в Firefox.

Изменить 2: более точно сравнить изображения. На самом деле кажется, что Firefox применяет некоторое улучшение изображения, но не отключает его при установке для imageSmoothingEnabled значения false.

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

Обходные пути на данный момент (с нетерпением жду ваших предложений!):

  • визуализировать холст с большим количеством пикселей, а затем уменьшить его с помощью CSS -> смещает курсор качества/производительности вручную
  • используйте автономный инструмент для изменения размера изображения -> не интерактивный
  • примените к изображению размытие на 1 пиксель -> больше нет зубчатых краев, но, очевидно, размытое изображение

Скриншот с техникой размытия:

У вас есть разрешение холста в коде как 640 на 360, но предоставленное вами изображение имеет размер 1382 на 750. Установите ширину и высоту CSS холста, чтобы они соответствовали ширине и высоте холста. например canvas{width:640px;height:360px;} Ширина и высота холста задают разрешение холста (количество содержащихся в нем пикселей), а ширина и высота CSS задают размер отображения холста (насколько он велик на странице). Для достижения наилучшего результата вы должны убедиться, что размер дисплея соответствует разрешению. .

Blindman67 22.12.2020 15:20

Спасибо за ваш ответ, но я просто увеличил свой браузер, чтобы показать проблему, проблема такая же, как и с холстом в разрешении 640x360. Я отредактирую свой вопрос, чтобы быть более явным.

Louis Coulet 22.12.2020 15:28

Ваш комментарий вдохновляет на обходной путь: визуализируйте холст размером 1280 x 720, а затем уменьшите его до 640 x 360 с помощью CSS. Таким образом, Firefox выполняет сглаживание, но я все же хотел бы иметь сглаживание напрямую через контекстный API.

Louis Coulet 22.12.2020 15:35

Тогда мало что можно сделать. FireFox не поддерживает ctx.imageSmoothingQuality = "high" Потеря качества связана с тем, что холст предназначен для скорости, а не для качества, и вы берете изображение с высоким разрешением и сжимаете его в холст с более низким разрешением. Если вы уменьшаете исходное изображение с помощью Photoshop (любой пакет для рисования) до Соответствуйте разрешению холста, вы получите лучшие результаты, так как пакеты с краской намного лучше справятся с уменьшением размера.

Blindman67 22.12.2020 15:50

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

Louis Coulet 22.12.2020 16:35

Изображение I .imageSmoothingEnabled корректно работает только в Firefox при увеличении изображения. Артефакты, которые вы видите, кажутся фундаментальной частью алгоритма масштабирования холста Firefox. Использование чего-то вроде ctx.save(); ctx.scale(1/6, 1/6); ctx.drawImage(img, 0, 0, 3840, 2160, 0, 0, 3840, 2160); ctx.restore(); дает тот же эффект.

Ouroborus 24.12.2020 16:32

Это ответ на ваш вопрос? stackoverflow.com/questions/17861447/…

Pavol Velky 24.12.2020 20:02

Или это? stackoverflow.com/questions/18922880/…

Kaiido 26.12.2020 04:12

Отвечает ли это на ваш вопрос? Html5 canvas drawImage: как применить сглаживание

Kaiido 05.01.2021 10:27

Спасибо за все ваши предложения! stackoverflow.com/questions/17861447 не отвечает на мой вопрос: применяет фильтр размытия, убирает неровные края, но результат получается размытым (хоть и тонким)

Louis Coulet 05.01.2021 15:35
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
10
995
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Образец пуха высокого качества.

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

Плюсы

Он имеет значительное преимущество с точки зрения качества, поскольку может использовать 64-битные числа JS с плавающей запятой, а не 32-битные числа с плавающей запятой, используемые графическим процессором. Это также уменьшает sRGB, а не более низкое качество RGB, используемое 2d API.

Минусы

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

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

Прирост качества для 99% людей, просматривающих результат, будет едва заметен.

Образцы площади

Метод выбирает исходные пиксели под новым целевым пикселем, вычисляя цвет на основе перекрывающихся областей пикселей.

Следующая иллюстрация поможет понять, как это работает.

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

Обзор процесса.

Сначала мы создаем 3 значения, чтобы удерживать новый цвет R, G, B равным нулю (черный).

Мы выполняем следующее для каждого пикселя под целевым пикселем.

  • Вычислите область перекрытия между целевым и исходным пикселями.
  • Разделите перекрытие исходных пикселей на площадь целевых пикселей, чтобы получить дробный вклад исходного пикселя в цвет целевых пикселей.
  • Преобразуйте исходный пиксель RGB в sRGB, нормализуйте и умножьте на дробный вклад, рассчитанный на предыдущем шаге, затем добавьте результат к сохраненным значениям R, G, B.

Когда все пиксели под новым пикселем обработаны, новые значения цветов R, G, B преобразуются обратно в RGB и добавляются к данным изображения.

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

Пример

Пример уменьшает изображение примерно на ~ 1/4.

После этого в примере отображается масштабированное изображение и изображения, масштабированные с помощью 2D API.

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

/* Image source By SharonPapierdreams - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=97564904 */


// reduceImage(img, w, h) 
// img is image to down sample. w, h is down sampled image size.
// returns down sampled image as a canvas. 
function reduceImage(img, w, h) {
    var x, y = 0, sx, sy, ssx, ssy, r, g, b, a;
    const RGB2sRGB = 2.2;  // this is an approximation of sRGB
    const sRGB2RGB = 1 / RGB2sRGB;
    const sRGBMax = 255 ** RGB2sRGB;

    const srcW = img.naturalWidth;
    const srcH = img.naturalHeight;
    const srcCan = Object.assign(document.createElement("canvas"), {width: srcW, height: srcH});
    const sCtx = srcCan.getContext("2d");
    const destCan = Object.assign(document.createElement("canvas"), {width: w, height: h});
    const dCtx = destCan.getContext("2d");
    sCtx.drawImage(img, 0 , 0);
    const srcData = sCtx.getImageData(0,0,srcW,srcH).data;
    const destData = dCtx.getImageData(0,0,w,h);

    // Warning if yStep or xStep span less than 2 pixels then there may be
    // banding artifacts in the image
    const xStep = srcW / w, yStep = srcH / h;
    if (xStep < 2 || yStep < 2) {console.warn("Downsample too low. Should be at least 50%");}
    const area = xStep * yStep
    const sD = srcData, dD = destData.data;

    
    while (y < h) {
        sy = y * yStep;
        x = 0;
        while (x < w) {
            sx = x * xStep;
            const ssyB = sy + yStep;
            const ssxR = sx + xStep;
            r = g = b = a = 0;
            ssy = sy | 0;
            while (ssy < ssyB) {
                const yy1 = ssy + 1;
                const yArea = yy1 > ssyB ? ssyB - ssy : ssy < sy ? 1 - (sy - ssy) : 1;
                ssx = sx | 0;
                while (ssx < ssxR) {
                    const xx1 = ssx + 1;
                    const xArea = xx1 > ssxR ? ssxR - ssx : ssx < sx ? 1 - (sx - ssx) : 1;
                    const srcContribution = (yArea * xArea) / area;
                    const idx = (ssy * srcW + ssx) * 4;
                    r += ((sD[idx  ] ** RGB2sRGB) / sRGBMax) * srcContribution;
                    g += ((sD[idx+1] ** RGB2sRGB) / sRGBMax) * srcContribution;
                    b += ((sD[idx+2] ** RGB2sRGB) / sRGBMax) * srcContribution;
                    a +=  (sD[idx+3] / 255) * srcContribution;
                    ssx += 1;
                }
                ssy += 1;
            }
            const idx = (y * w + x) * 4;
            dD[idx]   = (r * sRGBMax) ** sRGB2RGB;
            dD[idx+1] = (g * sRGBMax) ** sRGB2RGB;
            dD[idx+2] = (b * sRGBMax) ** sRGB2RGB;
            dD[idx+3] = a * 255;
            x += 1;
        }
        y += 1;
    }

    dCtx.putImageData(destData,0,0);
    return destCan;
}









const scaleBy = 1/3.964; 
const img = new Image;
img.crossOrigin = "Anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/7/71/800_Houston_St_Manhattan_KS_3.jpg";
img.addEventListener("load", () => {
    const downScaled = reduceImage(img, img.naturalWidth * scaleBy | 0, img.naturalHeight * scaleBy | 0);
    const downScaleByAPI = Object.assign(document.createElement("canvas"), {width: downScaled.width, height: downScaled.height});
    const ctx = downScaleByAPI.getContext("2d");
    ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
    const downScaleByAPI_B = Object.assign(document.createElement("canvas"), {width: downScaled.width, height: downScaled.height});
    const ctx1 = downScaleByAPI_B.getContext("2d");
    ctx1.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);    
    img1.appendChild(downScaled);
    img2.appendChild(downScaleByAPI_B);
    info2.textContent = "Original image " + img.naturalWidth + " by " + img.naturalHeight + "px Downsampled to " + ctx.canvas.width + " by " + ctx.canvas.height+ "px"
    var a = 0;
    img1.addEventListener("click", () => {
        if (a) {
            info.textContent = "High quality JS downsampler";
            img1.removeChild(downScaleByAPI);
            img1.appendChild(downScaled);   
        } else {            
            info.textContent = "Standard 2D API downsampler"; 
            img1.removeChild(downScaled);
            img1.appendChild(downScaleByAPI);            
        }
        a = (a + 1) % 2;
    })
}, {once: true})
body { font-family: arial }
<br>Click first image to switch between JS rendered and 2D API rendered versions<br><br>
<span id = "info2"></span><br><br>
<div id = "img1"> <span id = "info">High quality JS downsampler </span><br></div>
<div id = "img2"> Down sampled using 2D API<br></div>

Image source <cite><a href = "https://commons.wikimedia.org/w/index.php?curid=97564904">By SharonPapierdreams - Own work, CC BY-SA 4.0,</a></cite>

Подробнее о RGB V sRGB

sRGB — это цветовое пространство, которое все цифровые мультимедийные устройства используют для отображения контента. Люди видят логарифмическую яркость, что означает, что динамический диапазон устройства отображения составляет от 1 до ~ 200 000, что требует 18 бит на канал.

Буферы дисплея преодолевают это, сохраняя значения канала как sRGB. Яркость в диапазоне 0–255. Когда аппаратное обеспечение дисплея преобразует это значение в фотоны, оно сначала расширяет 255 значений, возводя их в степень 2,2, чтобы обеспечить необходимый широкий динамический диапазон.

Проблема в том, что обработка буфера дисплея (2D API) игнорирует это и не расширяет значения sRGB. Он обрабатывается как RGB, что приводит к неправильному смешиванию цветов.

На изображении показана разница между рендерингом sRGB и RGB (RGB, используемый 2D API).

Обратите внимание на темные пиксели в центре и на правом изображении. Это результат рендеринга RGB. Левое изображение рендерится с использованием sRGB и не теряет яркости.

@Kaiido Все цифровые медиа используют sRGB с тех пор. Мы не говорим об аппаратном обеспечении дисплея. Речь идет о рендеринге. Посмотрите на свой экран, увидите темные края при рисовании зеленым на красных линиях RGB, красный в смеси с зеленым не должен быть темнее (стремится к темно-красному или темно-зеленому). Это исправлено при рендеринге с использованием sRGB, имеющего тенденцию к желто-оранжевому цвету, чего 2D API НЕ МОЖЕТ СДЕЛАТЬ! .. Я добавлю пример к ответу.

Blindman67 26.12.2020 05:35

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

Kaiido 26.12.2020 05:46

@Kaiido Смотрите обновление на нижнем изображении ответа. 2D API явно не использует sRGB. Почему это так? Потому что «Композитные операторы Портера Даффа».drafts.fxtf.org/compositing-1/#porterduffcomposit‌​ingoperators Весь отображаемый контент добавляется на холст с помощью композитных операций по умолчанию «source-over». Операции Портера-Даффа — это RGB, а не sRGB. До тех пор, пока старый Porter Duff не будет заменен на композитинг с гамма-коррекцией, мы застряли в беспорядке рендеринга RGB.

Blindman67 26.12.2020 05:59

Спасибо @Blindman67 за этот подробный ответ, вы четко объясняете свой алгоритм для уменьшения изображения и даете рабочую реализацию javascript. Ваши пояснения по поводу sRGB также очень поучительны и убедительны. Теперь в моем конкретном случае, поскольку я хочу отображать только небольшие превью файлов изображений, я прибегаю к использованию CSS или масштабирования холста, чтобы он обрабатывался браузером.

Louis Coulet 27.12.2020 17:31

Что касается причины сглаживания псевдонимов в Firefox, вы говорите, что это потому, что алгоритм масштабирования указан W3C и не позволяет использовать изображения sRGB!? Если это так, похоже, Chrome решил не уважать его!

Louis Coulet 27.12.2020 17:35

@LouisCoulet Нет смысла, если sRGB (отсутствие его) является лишь частью проблемы. Проблема в том, что алгоритмы, используемые 2D API, не предназначены для таких высоких сокращений, поскольку они оптимизированы для повышения производительности.

Blindman67 28.12.2020 02:54

Другие вопросы по теме