Увеличение растровых изображений – какой максимальной схожести с оригиналом мы можем добиться и как? А можно побыстрее?

от автора

Эта статья является третьей из цикла Точное увеличение клип-артов c градиентом и мелкими деталями: #1,  #2

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

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

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

Теория и мысли

Нужно ли это и для чего?

Super-Resolution (увеличение разрешения) — достаточно молодая область, ей всего около 40 лет.

 Самые очевидные применения — обработка и анализ медицинских изображений, таких как рентген (чем чётче рентген – тем больше радиации) и МРТ

, обработка аэрокосмических снимков, обработка данных видеонаблюдения (когда по десятку пикселей надо определить что это), трансляция и хранение видеопотока (решаем проблему за счёт качества, а потом сами восстанавливаем качество) – особенно потенциально смотрится Wanjie Sun, Zhenzhong Chen. «Изучение уменьшения масштаба изображения для увеличения масштаба с использованием адаптивного ресемплера контента», удешевление оборудования — дешевые устройства имеют маленькую матрицу (например, тепловизор 32×24 пикселя стоит в 7 раз дешевле, чем 256х192), ускорение обработки графики (DLSS). Плюс в некоторых особо точных сферах (микроскопия, например) зачастую невозможно естественным образом увеличить разрешение, не уменьшив глубину цвета – принцип Гейзенберга (Значит ли это, что при увеличении растровых изображений мы обязательно потеряем в глубине?) или вообще невозможно получить изображение нужного разрешения без применения SR ввиду дифракционного предела.

Как видим, применения крайне разнообразны, но вот вопрос  на будущее – если не учитывать времязатраты, а только тип изображения — обязательно ли под каждую задачу нужен свой алгоритм или можно/нужно искать универсальный?

Практичность существующих методов по-прежнему пока сильно ограничена относительно низкой точностью  и затратами времени, а стратегии ускорения просто необходимы для крупномасштабных приложений. Прогресс алгоритмов Super-Resolution каждый год колоссальный, однако, идеального алгоритма нет и в ближайшие годы точно не будет. [1]

Нейросетевые лидеры увеличения по одному кадру

Нейросетевые лидеры увеличения по одному кадру

Идеального алгоритма, который полностью восстанавливает изображение как в CSI действительно не будет.

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

Здесь многие сдаются – «Зачем тогда пытаться изобрести вечный двигатель? Всё, что «восстанавливают» нейросети – это галлюцинации и этому нельзя верить!» Так ли это?

И да,  и нет:

1) Нет, так как до абсолютного восстановления до пределов Найквиста даже по многим кадрам всё ещё никто не добрался, даже если в теории по многим кадрам его можно преодолеть в 2 раза — очень близки, но можно совершенствоваться дальше.

Если идеала не существует — это не значит, что к нему не надо стремиться!

Просто хотя бы для того, чтобы узнать где предел — например, если мы увеличим изображение в 4 раза (то есть вместо одного пикселя появится 16) на какую точность мы можем рассчитывать? 6%? 25%? 90%?

2) Нет, так как естественное изображение (мы же не будем увеличивать бессмысленный белый шум?) — понятие более строгое, чем просто сигнал/информация. Так, в 1970-х годах, сейсмологи построили изображения уровней отражения внутри земли, основанные на данных, которые, не удовлетворяли критерию Найквиста — Шеннона. А в 2004 году математик Эмануель Канде открыл, что некоторые изображения магнитного резонанса (используется в МРТ) могут быть восстановлены точно даже с данными, которые считаются недостаточными в соответствии с критерием Найквиста‑Шеннона. [2]

3) Да, нейросети — зло, в том плане, что они дают нам искушение получить хороший результат быстро, но за это мы не получим понимание как это работает (они просто выдадут гору чисел в большой формуле, но как получены эти числа, что значат, пределы использования и т. п. — нет, это просто чёрный ящик) — в итоге мы не получаем решение, которое можем проверить, упростить (большинство узлов в нейросетях — просто мусор), улучшить. Так же, громадное значение при таком подходе имеет обучающая выборка — (если мы дадим НС только изображения китайцев, она будет из всех людей делать китайцев — а это, согласитесь странно). Также нейросеть сама по себе не скажет Вам в каком пикселе она точно уверена, а где галлюцинация. Поэтому, глубокие сверточные апскейлеры не следует использовать в аналитических приложениях с неоднозначными входными данными. [3][4] Эти методы могут искажать характеристики изображения, что может сделать их небезопасными, например, для медицинского использования. [5] Конечно же, существуют области, где использование нейросетей оправдано — например, в распознавании речи, вряд ли необходимо будет точно определить математически каждый звук, если результат устраивает. Также это может быть полезно как демонстрация практической возможности (Пятьдесят лет назад казалось невозможным заменить лицо на видео, чтобы большинство это сразу не заметило, однако мы не нашли решения — без нейросетей это сделать всё ещё нельзя)

Потом идёт непонимание того, что в Super-resolution есть два принципиально разных подхода:

Психо-визуальный подход (Beautification): Этот метод фокусируется на создании изображений, которые приятны для человеческого глаза. Алгоритмы нацелены на улучшение визуального восприятия, часто используя фильтры и техники, которые делают изображение более «естественным» и эстетически привлекательным. Здесь важна не столько точность восстановления, сколько субъективное восприятие качества. (Real-ESRGAN, например). Историки, узнав об этом, озлобляются на весь SR.

Точный восстановительный подход (High fidelity/Accuracy focused): Этот метод стремится максимально точно восстановить детали исходного изображения. Алгоритмы здесь используют сложные математические модели и нейронные сети для анализа и интерполяции пикселей, чтобы вернуть как можно больше информации о содержимом изображения. Цель — минимизировать искажения и сохранить оригинальные детали, даже если это может привести к менее приятному визуальному эффекту.

Дело в том, что просто цели и области сферы здесь разные:

Beautification (психо-визуальный подход):

Фотография и редактирование изображений: Используется в приложениях для редактирования фотографий, чтобы улучшать снимки и делать их более привлекательными. Но не используйте для восстановления исторических снимков!

Мода и косметика: Применяется в рекламных кампаниях и онлайн-магазинах для улучшения внешнего вида моделей и продуктов.

Социальные сети: Используется для обработки пользовательских фотографий, чтобы сделать их более привлекательными и визуально приятными.

Компьютерная графика и анимация: Используется для рендеринга качественных изображений с высоким разрешением из источников с низким, что позволяет создавать реалистичные сцены. В идеале ещё нужно делать это в режиме реального времени, так что ни о какой точности тут речи быть не может. На данный момент для этого требуются две RTX 4090 для получения максимального качества (но оптимизировав можно снизить до одной) либо GT 1030 для получения среднего качества:

Посмотреть разницу можно здесь

High-Fidelity Super-Resolution (точный восстановительный подход):

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

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

Космическая и спутниковая съемка: Применяется для обработки изображений Земли с высокими требованиями к детализации и точности.

 Далее у нас идут течения вида – «это возможно и нужно, но только по нескольким кадрам» — так называемый Video Super-Resolution:

Google Pixel 3 по нескольким кадрам качества А составляет один кадр качества Б

Google Pixel 3 по нескольким кадрам качества А составляет один кадр качества Б

«А Image Super-Resolution – это всё баловство для красоты, но не для точности». Тут тоже всё сложно:

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

— Нет, так как если использовать для восстановления только количество кадров, то имеется предел в теории 4х, на практике — 2x, иначе система становится переопределённой.

Таким образом, чистый VSR без ISR имеет кучу ограничений. Что тогда делают люди? Берут нейросети, тренированные на искусственно уменьшенных изображениях и на то, чтобы выдать красивую картинку (обратите внимание выше на картинке – субъективная оценка людьми) и удивляются, что появились галлюцинации.

Таким образом, в задаче повышение разрешения есть 16 разных подходов:

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

  • Нейросетевой (но каждый масштаб для них – это новая задача, поэтому в основном есть только 2x, 3x, 4x – а при их комбинировании получается лишняя работа и потом искажения ресайза) или математический (поэтому я выбираю его – всё что могут нейросети – может математика при наличии предварительной достаточной работы множества мозгов, а при сотрудничестве математики и нейросетей могут получиться ещё более лучшие результаты, например, если на вход подавать не только исходное изображение, но и предварительный апскейл или, например, если считать отход от того, что мы знаем об изображениях дополнительным штрафом)

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

  • По одному кадру (мне это интереснее) или нескольким

 Однако, хотя Image Super-Resolution имеет меньше информации, если двигаться в нужную сторону всё же может показать приличную точность – достаточно информации для восстановления уже содержится в изображении [6] — именно поэтому можно без потерь сжать изображение в 2-3 раза. То есть для апскейла уже есть информация, надо лишь её найти и сохранить, чтоб потом опять не искать — и некоторые факторы, которые могут помочь нам уже известны:

Что мы знаем?

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

  • Периметр краёв на изображениях тем больше, чем больше разрешение

(это как с береговой линией или границами государств – они тем больше, чем точнее измерения) Значит ли это, что надо максимизировать длину границ, искать кривую увеличение длины края/масштаб или только то, что ближайший сосед – не лучший метод?

(это как с береговой линией или границами государств – они тем больше, чем точнее измерения) Значит ли это, что надо максимизировать длину границ, искать кривую увеличение длины края/масштаб или только то, что ближайший сосед – не лучший метод?
  • В растровых изображениях распределение величины градиента (как сильно отличаются соседние пиксели) похоже на нормальное или Лапласа, причём пик (наиболее популярное значение градиента) очень близок к нулю:

  • По просьбам трудящихся и, в частности, @sergehog чёткая постановка задачи:

В общем виде задача SR определяется так:

LR = Decimation(Blur(HR)) + Noise, где LR – изображение низкого разрешения, Blur – некая функция размытия (несколько пикселей попадают в один), Decimation – некая функция прореживания (например, некоторые пиксели HR попали в перегородки между фотоэлементами)

Noise – шум внутри матрицы, а также отклонение между реальными значениями и дискретизированными по глубине цвета (8/16/24 бита) и нам надо найти HR, зная только LR. Как видим, задача не имеет чёткого единственного решения (Decimation может быть разным в зависимости от камеры, Blur – в зависимости от типа получения изображения LR – c камеры или уменьшенный определённым ПО, то есть заранее в общем случае нет параметров функций) – такая задача называется некорректной. То есть если даже мы согласны, что не можем получить абсолютно точный результат, нет даже чёткого решения проблемы.

Тем не менее, теория некорректных задач — активно развивающаяся, современная область научных знаний. Большинство практических задач некорректны, требуют принятия решений в условиях неопределенности, переопределенности или их противоречивости. Однако в сложившейся системе обучения математике некорректным задачам отведено незначительное место, вопреки потребностям практики. Не вызывает сомнений тот факт, что нельзя научиться решать некорректные задачи, обучаясь лишь на корректных. [7] Стоит отметить, что нейросети уже научились достаточно точно решать часть таких задач (например waifu2x – восстанавливает кадры из 2d-мультфильмов не так качественно, как современные сети, но гораздо точнее их. Правда в таких кадрах есть дополнительное условие – заранее известно, что там чёткие края и однотонная заливка, что очень легко, однако реальный мир оперирует полунепрерывностями)

Однако, я думаю, надо сначала решить задачу попроще, прежде чем продвигаться к более сложной:

Так как задача некорректная, многие исследования просто предполагают, что LR — это бикубически уменьшенная версия HR или просто средние значения пикселей HR в одном пикселе LR – в любом случае

\int_{xs-s/2}^{xs+s/2}\,dX \int_{ys-s/2}^{ys+s/2}HR_X,_Y dY≈LR_x,_y

, где s – масштаб увеличения

Если бы x и y были рациональными числами, то задача бы сводилась к деконволюции (Deblur)

И можно было бы достаточно точно найти HR как LR-LR’’ (вторая производная), но здесь у нас растровое изображение и x,y – натуральные числа.

Зная вышесказанное:

Простая задача представляется как: зная матрицу LR (m х n) и масштаб увеличения s найти такую матрицу HR (ms х ns), что:

1)

\left|\frac{\sum_{X=xs}^{xs+s-1}\sum_{Y=ys}^{ys+s-1}HR_{X,Y}}{s^2} -LR_{x,y}\right|<αx ∈[0;m)∩N,y∈[0;n)∩N

Отклонение от среднего не больше, чем α

2) Минимизовать

\min_{{{a ∈(-∞;0)∩R \atop b ∈ R} \atop {c ∈ R \atop HR}} \atop {{k ∈ (0;+∞)}}} \sum_{i=0}^{361} (|\{X ∈(0;ms-1]∩N,Y∈(0;ns-1]∩N|i-0.5≤\sqrt{(HR_{X,Y}-HR_{X+1,Y})^2+(HR_{X,Y}-HR_{X,Y+1})^2}≤i+0.5\}|-k*e^{a*i^2+bi+c})^2-\frac{b}{2aβ}

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

Если накосячил где-то в формулах – поправьте

3)

1)      Найти при каких α и β (в зависимости от масштаба увеличения) средняя ошибка от оригинала минимальна

 При 128x – 3x увеличении α на оригинале варьируется около 36-181, что не близко к нулю, но и далеко от 255, что значит, среднее что-то даёт, но полагаться только на него бессмысленно.

 Однако, благодаря обратной корректировке, которая теперь доступна здесь, можно не задумываться о среднем вообще, она любую HRкартинку превратит во что-то похожее на LR:

, но правда там немного другие значения α:

raMagLanc1: 34-163

raLanc1: 37-169

raLanc2.5: 44-182

И поэтому ожидать MAE больше 0,9 не стоит.

Эта простая задача итак NP-полная, так что в идеале нужно придумать ещё что-то… To be continued… Про это подробнее я расскажу в следующих статьях, а здесь далее продолжение наработок прошлых.

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

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

Практические проблемы

Оптимизация на C# для новичков в этом деле

Написать код, который работает  — можно достаточно быстро

Написать код, который работает как можно быстрее – достаточно долго

Дисклеймер

Я такой же новичок и дальнейшие рекомендации предназначены исключительно для тех новичков в оптимизации, кто хочет наибольшего ускорения, которое можно получить, не особо напрягаясь. Если Вы профессионал в оптимизации – можете дальше не читать, Вы знаете больше меня (ну или можете исправить/добавить рекомендации в комментариях). Всё проверено только на моём ПО по апскейлу – https://github.com/no4ni/scaleSmooth/ – и моём ПК (i3-10100F, DDR4, GTX 1050 Ti (ISA 8.2, Capabilty 6.1)), так что:

  • Эти советы могут дать лишь общее представление того, что можно попробовать по-быстрому сделать на C# (в других языках – приёмов больше), однако если что-то из очевидного не указано ни здесь, ни в комментариях – скорее всего это на C# не сработает так, как Вам хочется, так как тут есть встроенные оптимизации, в которые лучше не лезть, даже если Вы знаете, как быстрее умножить на 2

  • Вдобавок прилагается примерное не теоретическое, а измеренное в реальных условиях ускорение – чего можно ожидать от того или иного приёма (-3% +1% — значит приём в некоторых случаях может дать ускорение на 3%, но в других может и замедление на один)

  • После применения каждого – желательно проверять результат, обязательно там, где можно уйти в + по разнице во времени.

Лучше конечно сразу переписать на C (-68%-75%) или C++ (-50%), но:

  • Нужно вручную удалять переменные

  • Отладка дикая (процесс может не вылететь с ошибкой и её описанием, а продолжить работу неадекватно, например, выдавать неправильные данные или показать синий экран https://habr.com/ru/articles/830280/)

Можно этого избежать, если переписать на Rust(-67%), но что там, что там есть другие минусы:

  • Это долго (особенно если Вы знаете только С#)

  • Надо будет сначала (Вам или пользователю) скомпилировать варианты для каждой/своей архитектуры процессора, ГПУ, ОС и разрядности, а так можно библиотеку сделать для популярных платформ и компилятор на лету может оптимизировать код под конкретный клиентский процессор и ОС, ГПУ (с помощью ILGPU), предоставляя по мере возможности максимальную производительность) 

  • С таким же успехом можно всё подряд писать на ассемблере

Либо можно использовать полумеры через unsafe{код на C++} в отдельных местах (-20% времени в этих местах), но это надо разбираться как минимум в указателях.

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

Парето по моим методам из прошлых статей (пунктир - старая версия, сплошная - новая)

Парето по моим методам из прошлых статей (пунктир — старая версия, сплошная — новая)

Однако прежде чем что-либо оптимизировать на быстродействие, нужно помнить, что это чёрная магия и плата за её использование велика – нечитаемый код (DRY идёт лесом), увеличение расходов памяти в несколько раз, уменьшение точности (хотя у меня конкретно почему-то точность наоборот возросла), ускорение долгих процессов, но замедление мгновенных и т.д. Если Вы всё ещё хотите именно ускорения, а не удобства написания кода, сокращения размеров программы (что тоже может увеличить производительность) и требуемой ОЗУ, то:

Общие советы:

  • Проанализируйте, правда ли, что больше итераций даёт лучший результат и насколько это оправдано (-99% в крайних случаях)

  • Сделайте несколько реализаций (например, для маленького количества итераций, для большого, на  CPU, на GPU, с каким-то приёмом оптимизации, без него), проанализируйте и применяйте конкретную в каждом случае. (-?%)

  • Заменяйте обычное деление на переменную умножением там, где это возможно и количество операций от этого не сильно увеличится (-7% +3%) (не путайте целочисленное деление с обычным)

  • Уберите ненужные присваивания (-7% +10%) (компилятор не всегда показывает)

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

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

    Одна и та же функция при разных аргументах (для долгих процессов кеширование перестаёт работать)
  • Если более половины Вашей задачи выполняется параллельно, Вы упёрлись в потолок CPU (более 97% загрузки ЦП) и выполнение занимает более 0,7-1 секунды, используйте вместо CPU видеокарту (GPU), например, с помощью библиотеки (nuget-пакета) ILGPU (поддерживаются почти все современные видеокарты, а в случае их отсутствия код продолжит выполняться на CPU) (-95% в среднем, но +913% в некоторых случаях) (возможна погрешность около 0,09%-0,15%). Для более быстрого исполнения на GPU (если у Вас всё параллельно) – используйте шейдеры, там не важно сколько занимает выполнение на CPU — идеально походит для оптимизации коротких процессов.

  • Точно указывайте тип возвращаемого значения (до -27%)

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

for(int x=0; x<w; x++){   for(int y=0; y<h; y++){     Color pixel = bmp.GetPixel(x,y);     rgbArr[x,y,0]=pixel.R;     rgbArr[x,y,1]=pixel.G;     rgbArr[x,y,2]=pixel.B;   } }  

что-то типа:

BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadWrite, bmp.PixelFormat); int stride = Math.Abs(bmpData.Stride); int bytes = stride * bmp.Height; IntPtr ptr = bmpData.Scan0; byte[] rgbValues = new byte[bytes]; Marshal.Copy(ptr, rgbValues, 0, bytes);  if (bmp.PixelFormat == PixelFormat.Format24bppRgb) {     int w3 = w * 3;     int stridew3 = stride - w3;     Parallel.For(0, rgbValues.Length, c =>     {         if (c % stride < w3)         {             int realc = c - stridew3 * (c / stride);             int realc3 = realc / 3;             int x = realc3 % w;             int y = realc3 / w;             rgbArr[x, y, 2 - realc % 3] = rgbValues[c];         }      }); } else {     int w4 = w * 4;     int stridew4 = stride - w4;     Parallel.For(0, rgbValues.Length, c =>     {         if (c % stride < w4)         {             int realc = c - stridew4 * (c / stride);             int realc4 = realc / 4;             int x = realc4 % w;             int y = realc4 / w;             int realcm4 = realc % 4;             if (realcm4 < 3) rgbArr[x, y, 2 - realcm4] = rgbValues[c];         }      }); } bmp.UnlockBits(bmpData);

(-20% времени)

  • А для перерисовки всего изображения вместо

for(int x=0; x<w; x++){   for(int y=0; y<h; y++){     bmp.SetPixel(x,y,Color.FromRgb(red[x,y],green[x,y],blue[x,y]));   } }

можно сделать что-то вида

BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadWrite, bmp.PixelFormat); int bytes = w * h * 4; IntPtr ptr = bmpData.Scan0; byte[] rgbValues = new byte[bytes]; Parallel.For(0, rgbValues.Length / 4, c => {     int c4 = c * 4;     int x = c % w;     int y = c / w;     rgbValues[c4] = blue[x, y];     rgbValues[c4 + 1] = green[x, y];     rgbValues[c4 + 2] = red[x, y];     rgbValues[c4 + 3] = 255; }); Marshal.Copy(rgbValues, 0, ptr, bytes); bmp.UnlockBits(bmpData);

(-10% времени)

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

Советы для GPU:

  • Обязательно просчитывайте всё, что можно заранее, общие случаи разбивайте на частные и там предпросчитайте сами что чему равно в этих случаях, упрощайте выражения на сколько это возможно (до -50%)

  • Вместо double используйте float, Math -> MathF (-97%), 0.5 -> 0.5f (-1,5%)

  • Везде, где можно ставьте static (даже в анонимных функциях) (-7%)

Советы для CPU:

  • Насчёт SIMD, таких как SSE и AVX у меня мнение неоднозначное:

С одной стороны в C# это не всегда уместно. Иногда только в небольших и итак быстрых функциях, которые часто вызываются (-1% при неумелом использовании или до -24%, если хотите сломать себе мозг), а в остальном, где можно, С# сам постарается оптимизировать (особенно если у Вас итак есть векторные вычисления). Поэтому стоит ли это использовать новичкам в любых реальных проектах с параллелизацией на C#, а не в образцовых задачах и однотипных (с т.з. вычислений) играх? Ведь отсекается часть процессоров (в 2024 году у 3% пользователей на ПК, за которым они сейчас сидят, может не быть SSE2, а у 5% — AVX2) https://pikabu.ru/story/moya_borba_s_otsutstviem_instruktsii_sse_42_v_protsessore_10675135

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

Новичкам лучше соваться сюда не надо. Это хорошо в С, где каждый тип Вы с нуля написали сами, а если всё же выбрали С# — просто доверьтесь ему.

С другой стороны, если Вам кровь из носа нужно вырвать пару миллисекунд – тогда у Вас выбора нет — становитесь профессионалом в SIMD (а лучше ещё и в C заодно) и поддерживайте и компилируйте несколько версий.

  • Использовать явный тип вместо var (-3%)

  • Используйте Parallel.For вместо for, там где это не итерация, а просто много вычислений (-60%)

  • Parallel.For повыше (может дать -50%), но не пугайтесь вложенных Parallel.For (в начале корутины они действительно могут лишь увеличить время выполнения, но ближе к концу они иногда могут давать до -4%) 

  • Если же корутины примерно одинаковы по времени, определите места которые можно выполнять параллельно – получится несколько вложенных Parallel.For, после чего реорганизуйте их в один Parallel.For

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

    Выбор стратегии параллелизации должен учитывать соотношение между размером задачи и количеством процессоров. Также можно рассмотреть замену верхнего Parallel.For на for, если maxY всегда велико при малом maxX (-3%, -8% в крайних случаях)

    Выбор стратегии параллелизации должен учитывать соотношение между размером задачи и количеством процессоров. Также можно рассмотреть замену верхнего Parallel.For на for, если maxY всегда велико при малом maxX (-3%, -8% в крайних случаях)
  • Cохранение промежуточных переменных (тех, которые пригодятся, но их можно заново рассчитать) логично (например, кэширование результатов дорогостоящих функций), но излишнее кэширование всего подряд может быть наоборот вредным  (-30% +13%)

  • double -> float; (уменьшение точности) 1/Math.Pow(x, k) -> n=-k; MathF.Pow(x, n); (-14-22%), 0.5 -> 0.5f(-2%)

  • Не забывайте про const, если переменные не меняются во время выполнения, а меняются только иногда разработчиком (-2%)

  • Везде, где можно ставьте static в объявлении функций (до -3%)

  • Вместо цикла с несколькими условиями сделайте несколько циклов без условий, если это возможно (-1% -4%),

    Другие оптимизации могут оказаться наоборот вредны (что на C хорошо, то на С# при недостаточном понимании может быть плохо, так как есть встроенные оптимизации, которые Вы можете сломать) (в частности, если суммарное кол-во операций уменьшится, но размер программы увеличится), но если есть желание – вот теория для любого языка — проверяйте

    В следующей статье будет инструкция по созданию пакета из набора функций для встраивания в несколько проектов

    Новые методы

    Начнём с вариаций методов из прошлых статей:

    1.    scaleSmooth (сгладить ближайшего соседа, не меняя среднего значения исходного пикселя) разделился на несколько вариантов:

    SmoothContinuous — изначальный вариант

    Smooth – загибает края в противоположную сторону насколько это возможно с условием среднего, чтобы закончить объект до края

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

    2.    ContrastBold (устанавливать значение пикселя не как среднее от окружающих, а немного ближе к границе допустимых значений – то есть не тёмно-серое, а тёмно-тёмно-серое)

    дополнился не такой контрастной версией Bold:

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

    BA – изначальный вариант с немного скорректированными параметрами (больше сохранение среднего значения исходного пикселя, чем их производной)

    BAContrast – просто добавил немного контраста для выделения краёв и снижения звона у границ допустимых значений (0-255)

    BASmoothContrast – ещё добавил, чтобы сильно далёкие от места события интерполяции почти не учитывались

    BAMonochrome – попытка после BA отсортировать все значения на тёмные, серые и светлые (в каждом канале), после чего установить тёмные как чёрные, светлые как белые, а серые (большинство) в зависимости от того к каким значениям они ближе в пространстве — будет полезно для монохромных изображений. BAMonochrome2 – то же самое, только после BASmoothContrast и серых столько же, как белых или чёрных

    255BA – то же самое, как BAMonochrome, но после 255 BA (каждый раз подаём монохромное изображение со своим порогом от исходного) для каждого канала

    Thin255BA — то же самое, как BAMonochrome2, но после 255 BASmoothContrast (каждый раз подаём монохромное изображение со своим порогом от исходного) для каждого канала и серых в 2 раза больше

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

    BADerivative – BA + отклонение от среднего значения (127) BA, нацеленного на сохранение производных

    Далее новые:

    SmoothCAS – начинаем с ближайшего соседа и минимизируем градиент по каждому каналу с помощью градиентного спуска, не забывая после каждой итерации спуска возвращать среднее значение исходного пикселя в пределах дискретизации, после чего добавляем контраст, но больше там, где больше средний градиент по каналам в исходном изображении и не добавляем контраст там, где окружающие исходные пиксели такие же, как сам исходный пиксель (Contrast Adaptive Sharpening). При достаточном количестве итераций минимизации градиента из ступенчатой интерполяции получается непрерывное изображение. Градиент исходного изображения билинейно интерполируется, что также даёт непрерывность. В итоге получается непрерывное, но чёткое изображение.

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

    Эти и старые методы можно пощупать и изменить на https://github.com/no4ni/scaleSmooth/

    Анализ

    Несмотря на то, что я разрабатываю слепые математические методы апскейла, направленные на точность по одному кадру, анализ я веду среди всех методов. Итак, на этот раз к 2 серым изображениям из прошлого этапа добавились 2 новых цветных (все они в стиле клипартов с наличием градиента и мелких деталей). Тут надо сказать, что данный анализ в данном цикле имеет ограничения – его результаты подходят не для всех изображений, а только клип-артов и то не всех:

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

Кто знает, какое уменьшение использует Paint – отпишитесь (сразу скажу – это что-то не стандартное)

Кто знает, какое уменьшение использует Paint – отпишитесь (сразу скажу – это что-то не стандартное)

Посмотрим, как точно и быстро справляются мои старые, но оптимизированные методы, а также стандартные и победители 2 прошлых этапов (топ-6) с корректировкой raLanc1 и без неё:

Анализ произведён на 4 клипартах с градиентом (8х8 x128 gray, 16х16 x35 gray, 32x32 x16 color, 640x360 x3 color). У raLanc1 есть свой предел – около 90%, есть подозрение, что это именно из-за того, что эта корректировка использует окно Ланцоша, но это мы решим после всех этапов. Smooth местами лучше стандартных методов (Билинейная, Бикубическая), но они все немного не дотягивают до кривой Парето.

Анализ произведён на 4 клипартах с градиентом (8х8 x128 gray, 16х16 x35 gray, 32×32 x16 color, 640×360 x3 color). У raLanc1 есть свой предел – около 90%, есть подозрение, что это именно из-за того, что эта корректировка использует окно Ланцоша, но это мы решим после всех этапов. Smooth местами лучше стандартных методов (Билинейная, Бикубическая), но они все немного не дотягивают до кривой Парето.

Теперь оставим только кривую Парето и попробуем корректировку raLanc2.5:

Она лишь сильнее приближает к 90-90,5%

Теперь попробуем лучшие (без корректировок и с корректировками raLanc1 и raLanc2.5) вариации оставшихся методов, методов не победителей, но претендентов прошлых этапов и достойные из моих новых методов:

Благодаря разделению методов на серый (1 канал) и цветной (3 канала) SmoothContrast оказался быстрее стандартного прямоугольного окна (ближайший сосед), если мы изначально знаем, сколько значащих каналов в изображении. И это в принципе можно внедрить и в ближайшего соседа, а если ещё и оптимизировать его всеми вышеописанными методами, то получится FastNearestNeighbour. Оставим только кривую Парето и посмотрим на FastNearestNeighbour, а также на некоторые чужие, которые мы не рассматривали в предыдущих этапах в лучших вариантах:

Как видим, оптимизировать можно и нужно всё подряд – сравните NearestNeighbour GDI+, Прямоугольное окно и мой FastNearestNeighbour

А теперь оставим только кривую Парето:

Итого пока всего 8 достойных методов:

  • unlimited:waifu2x / swin_unet / art – Нейросеть специально для рисунков. Пока смог запустить только через браузер. Показывает наибольшую точность, обратная корректировка не требуется. В планах запустить её без браузера на GPU (кто знает, как это сделать — отпишитесь). TTA4 не прибавляет точности, так что рекомендую использовать TTA2 для ускорения.

  • waifu2x / Artwork – Также нейросеть специально для рисунков, так как запускается прямо на GPU намного быстрее. Точность от использования float не стоит дополнительного времени, так что рекомендую использовать FP16 для ускорения. Обратная корректировка не требуется.

  • waifu2x-caffe / UpRGB – Это waifu2x, ускоренный с помощью caffe. Модель upconv7 для аниме. TTA здесь не прибавляет точности. С обратной корректировкой raLanc1 немного точнее.

  • Photoshop / Preserve details 2.0 (также нейросеть) с обратной корректировкой raLanc1

  • На данный момент кажется, для апскейла ничего оптимальнее интерполяции Ланцоша с 1979 года человечество ещё не поняло, хотя как мы уже разбирали в предыдущих статьях интерполяция – не то, что нам надо в идеале. Изобрело нейросети, но не может объяснить их принцип передискретизации. Фильтр был изобретен Клодом Дюшоном , который назвал его в честь Корнелиуса Ланцоша из-за использования Дюшоном сигма-аппроксимации, созданной Ланцошем. Поскольку это интерполяция любые обратные корректировки приветствуются. Единственное, что могу здесь сказать, не используйте a>2.5 (например, Lanczos3)

  • sharpened_bilinear – Пользователь @orekh поделился своим вариантом передискретизации, не являющийся интерполяцией, который ищет такое изображение с исходным разрешением (m x n), которое бы при увеличении билинейной интерполяцией с краевой сеткой до требуемых размеров (HRms x ns) без пределов значений удовлетворяло бы всё той же

\left|\frac{\sum_{X=xs}^{xs+s-1}\sum_{Y=ys}^{ys+s-1}HR_{X,Y}}{s^2} -LR_{x,y}\right|<αx ∈[0;m)∩N,y∈[0;n)∩N

, где α пренебрежительно мало. Результатом является HR после своей корректировки. Исполняемый файл и мини-инструкцию как пользоваться можно найди здесь

  • ImageResizer / ReverseAA – ещё одна реализация не интерполяции, работающая с соседними пикселями, чтобы убрать воздействие сглаживания, образующееся при уменьшении на GPU

  • ScaleSmooth / FastNearestNeighbour – моя версия ближайшего соседа, которая, похоже, быстрее всего, что может быть, особенно если мы пока считаем от Bitmap (представление изображения в .NET) LR до Bitmap HR

    А теперь посмотрим на зависимость точности от скорости:

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

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

Но это всё на среднем масштабе 4x-45x, а теперь посмотрим на зависимость точности от масштаба:

Последнее число в формуле близко к нулю, но больше него – значит можно ещё немного лучше, чем swin_unet/art

Последнее число в формуле близко к нулю, но больше него – значит можно ещё немного лучше, чем swin_unet/art

Отвечая на вышепоставленный вопрос – если увеличить изображение в 4 раза можно рассчитывать на точность в среднем от 37% до 96% (даже если мы не считаем то, что мы можем угадать как выпадет монетка в 50% случаев точностью) в зависимости от того, каким временем мы располагаем, особенно если мы знаем в каком стиле оно должно быть. Значит ли это, что при соответствующем уменьшении теряется лишь 4% информации, если мы знаем, что это изображение, а не шум или кляксы?

А если же нам гораздо важнее точность, чем время, то порядок немного изменится. Вот значения средней нормализованной ошибки MAE (аниме от 2,87% до 11,69%, ну погоди 5,81%-10,36%, математика 8,32%-18,58%, волк ШБ 14,30%-20,46%), где 0% — всегда самый точный метод, 100% — всегда самый неточный:

Самый точный из проанализированных unlimited:waifu2x / swin_unet / art TTA2 64 Highest

4,54%

 waifu2x / Artwork 64-64 TTA Highest

5,54%

unlimited:waifu2x / cunet / art TTA2 64 Highest

9,70%

waifu2x / Artwork 64-64 TTA None

9,88%

waifu2x-caffe / СUnet 64-20 TTA Только увеличить

12,59%

Начинает требоваться обратная корректировка waifu2x-caffe / upRGB 64-20 Только увеличить + raMagLanc1

12,64%

waifu2x / upconv7 / art 64-64 TTA None FP16 + raLanc1

13,21%

waifu2x-caffe / Фото+Аниме 64-20 TTA Только увеличить

13,76%

waifu2x-caffe / UpResNet10 64-20 TTA Только увеличить + raMagLanc1

14,18%

waifu2x-caffe / UpResNet10 64-20 TTA Только увеличить

14,19%

waifu2x / cunet / art TTA 64-64 FP16 Highest

14,28%

waifu2x-caffe RGB 128-20 TTA Только увеличить

16,36%

waifu2x-caffe Y 64-20 TTA Только увеличить + raMagLanc1

16,71%

Photoshop / Сохранение деталей 2.0 / Уменьшить шум 100% + raLanc2.5

19,11%

Самый точный не нейросетевой метод  — Photoshop / Сохранение деталей (с увеличением) / Уменьшить шум 100% + raLanc1

21,44%

 GIMP / Super-xBR(py) + raLanc2.5

22,06%

Ланцош a=2,24 + raLanc2.5

23,35%

Cамый точный мой метод scaleSmooth / BA / GPU + raLanc2,5

23,75%

StableDiffusion1.5 / img2img Text prompt LCM Automatic 150 Just resize Denoise 0 CFG 7 + raLanc2.5

24,36%

StableDiffusion1.5 /img2img No text prompt Euler a Karras 20 Just resize Denoise 0 CFG 7 + raLanc2.5

24,50%

Ланцош a=2,5 + raMagLanc1

24,69%

sharpened_bilinear + raMagLanc1

25,19%

scaleSmooth / AntiBicubuc + raLanc1

26,00%

Новая версия метода на 5 пунктов точнее старой — scaleSmooth / Smooth + raMagLanc1

26,20%

scaleSmooth / BASmoothContrast / GPU + raLanc1

27,26%

scaleSmooth / SmoothContinuos + raMagLanc1

27,61%

scaleSmooth / BAContrast / GPU + raLanc1

27,88%

https://github.com/Hawkynt/2dimagefilter ReverseAA Wrap OpenGL

27,98%

scaleSmooth-v1.8.0 / scaleSmooth + raMagLanc1

28,65%

Ланцош радиусом 1 пиксель + raMagLanc1

28,70%

DeepResize(Trial) OFF 100 – возможно точнее, дайте ключ, если кто может, чтобы протестировать + raMagLanc1

29,41%

scaleSmooth / Bold + raMagLanc1

30,20%

scaleSmooth / BAMonochrome2 / GPU + raLanc1

30,80%

scaleSmooth / 255BA / GPU + raLanc2.5

30,99%

StableDiffusion1.5 /img2img No text prompt, Just resize, Euler a, Karras, 20, 7, 0.35, Ultimate SD upscale, R-ESRGAN 4x+, Chess, 512, 12 + raLanc1

31,73%

Окно Ханна + raLanc2.5

31,99%

scaleSmooth / SmoothContrast + raMagLanc1

32,26%

scaleSmooth / ContrastBold + raLanc1

33,88%

StableDiffusion1.5 /img2img No text prompt, Just resize, Euler a, Karras, 20, 7, 0.35, Ultimate SD upscale, R-ESRGAN 4x+ Anime6B, Chess, 512, 12 + raLanc1

34,51%

scaleSmooth / Thin255BA / CPU + raMagLanc1

35,78%

scaleSmooth / SmoothCAS + raMagLanc1

37,14%

scaleSmooth / BAMonochrome / GPU + raLanc2,5

38,38%

scaleSmooth / DerivativeBA / CPU + raLanc1

54,41%

scaleSmooth / BAExtremum / CPU + raLanc1

65,55%

Таким образом, вот что мне удалось получить:

Для каждой картинки – самый точный метод разный

Для каждой картинки – самый точный метод разный

Все нейросети для апскейла проанализировать невозможно – они плодятся тысячами каждый день, но если Вы считаете, что нашли более быструю/точную нейросеть для апскейла клипартов/аниме – пишите в комментариях, постараюсь проанализировать. А так уже в планах:

  • Выяснить точное значение a для фильтра Ланцоша, наиболее подходящее под клип-арты

  • Попробовать, используя разные параметры, задать prompt для Stable Diffusion, чтобы он нарисовал оригинальную картинку

  • Использовать другие нейросети, такие как DAT, ESRGAN, LDSR, ScuNET, SwinIR, Universal Upscaler, UltraSharp, NMKD, Lollypop, AnimeSharp, Swin2SR, waifu Artwork/Scans, Nnedi3, Instant 4K, шейдеры для VSR, EDSR, Magnific Upscaler & Enhancer, MDSR, SRGAN, VDSR, FSRCNNX, Anime4K, RealSR, ACnet, DRCT, HAT, DUF, PSRT, VRT, RVRT, SRMD, DBVSR, D3Dnetб BigJpg, Ultra 2xSCL, Super 2xSCL, 2xSCL, Eagle3XB, SR3, PSSR, BSRDM, HDRI, SinGAN, VEAI, RealBasicVSR, BSRGAN, RealEsrgan, RealSR, ESPCN, TMNet, BasicSR, DBVSR, LGFN, GFPGAN, SAFMN, SODA-SR, pixelation_correction, BasicVSR, RRN, SR3, cMancio00/Super-Resolution, RAISR, VDSR, EDSR,

  • Отправить к ним в поддержку ControlNet и Refiner

  • Попробовать опцию Shuffle у unlimited:waifu2x

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

  • Проанализировать методы для пиксель-арта: DES2, XBRz, ScaleFX

  • Попросить ChatGPT и DALL-E нарисовать оригинал

  • Поиграться с адаптивной резкостью и гамма-коррекцией, рядами Фурье, подбирать градиенты близкие к нормальным, соединять линиями/треугольниками наиболее близкие по значению пиксели, линейная интерполяция в гилбертовом пространстве, интерполяция кривыми Безье в трёхмерном пространстве для сохранения производной, проверить что будет, если не сразу увеличивать, а постепенно, просто заблюрить ближайшего соседа, поиграться с деконволюцией, после или до увеличения понизить глубину цвета, перед увеличением вычесть доминирующую поверхность, чтобы остались только детали, попытаться решить задачу через матрицы, декодировать JPEG в большем разрешении, найти как утоньшить Собеля, интерполяция Уиттекера-Шеннона, глобальным кубическим сплайном, Акимой, использовать предсказание по контексту, создать мозаику из треугольников, которая удовлетворяет среднему, использовать фрактальную рекурсию,

  • Проанализировать методы программ: Qimage Ultimate, Paint.net, PhotoZoom Pro, AI Image Enlarger, Let’s Enhance, ФотоМАСТЕР, MindOnMap, Fotor, Snapseed, Genuine Fractals, Topaz Gigapixel AI, Image Analyzer, Exposure Blow Up, ON1 Resize, BiaPy, Davinci Resolve Super Scale, MagPie, Remini,  ImageMagick, AnyRec AI Image Upscaler, Adobe Lightroom Super-Resolution

  • Попробовать перевести в вектор

  • Проанализировать не нейросетевые методы superResolution_sparseRepresentation

  • Попробовать методы Glasner,Bagon,Irani — SR from a single image, Копфа-Лищински, SoftCuts

  • Попробовать разобраться, что же именно делает какая-нибудь сеть waifu (кто знает как — пишите)

Но это всё займёт время – стоит ли?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Работает ли релиз scaleSmooth? https://github.com/no4ni/scaleSmooth/

100% Win10+ x64, да1
0% Win10+ x32, нет0
0% Win8 x64, да0
0% Win8 x32, нет0
0% Win7 x64, да0
0% Win7 x32, нет0

Проголосовал 1 пользователь. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/872840/