Матричные преобразования 🔢 на C# для редактирования изображений
Учимся вращать, растягивать, отражать и перекрашивать картинки с помощью основ линейной алгебры и C#. Формируем матрицы трансформаций и проверяем результат в демо-приложении.
В этом руководстве мы научимся применять простейшие трансформации изображений в приложениях на языке C#. Для этого реализуем в коде базовые концепции линейной алгебры: умножение матриц, скалярное произведение векторов и применение матриц трансформации к пикселям. Дочитав статью, вы будете знать, как повернуть, растянуть, отразить и перекрасить картинку прямо из кода на C#.
Умножение матриц
Матричное умножение – довольно незамысловатая операция. Реализуем функцию умножения матриц в коде. Если вам не хватает знаний линейной алгебры, наглядное представление можно получить из нашей публикации. Для начала создадим C#-класс Matrices и его основной метод Multiply:
Мы начинаем с определения размеров обеих матриц с помощью метода Array.GetLength(). Записываем полученные результаты в переменные, чтобы не пришлось их заново вычислять. Далее убеждаемся, что полученные матрицы в принципе можно перемножить: число столбцов первой матрицы должно равняться количеству строк второй, иначе выбрасываем исключение.
Произведение двух матриц
Произведение двух матриц – это тоже матрица. Количество строк в ней равно числу строк первого множителя, а количество столбцов – числу столбцов второго множителя.
На следующем шаге создаем заготовку выходной матрицы нужного размера. Наконец, нужно заполнить каждую ячейку матрицы-произведения. Для этого последовательно умножаем каждый ряд первой матрицы на каждый столбец второй. По сути, каждый ряд и каждая колонка – это вектор, скалярное произведение которых дает нам соответствующий элемент итоговой матрицы.
Скалярное произведение векторов
Скалярным произведением двух векторов называется сумма попарных произведений соответствующих координат: x1*x2 + y1*y2 + z1*z2.
Трансформации
Теперь можно использовать алгоритм умножения для того, чтобы создать матрицу трансформации изображения. Эту матрицу затем нужно последовательно применить к каждой точке изображения (X, Y) или к ее цвету (ARGB). В результате мы получим новое – трансформированное – изображение.
Начнем с определения абстрактного интерфейса трансформации IImageTransformation с двумя членами: методом CreateTransformationMatrix() и булевым свойством IsColorTransformation.
Теперь разберемся, что нужно сделать с пикселями исходного изображения, чтобы применить к нему ту или иную трансформацию.
Вращение
На основе приведенной выше матрицы поворота напишем класс, отвечающий за вращение:
Функции Sin() и Cos() принимают углы в радианах, поэтому мы добавили две дополнительные функции для конвертации градусов в радианы и обратно, чтобы упростить взаимодействие для конечного пользователя класса.
Вторая популярная трансформация – масштабирование по определенному коэффициенту. Она работает путем простого умножения нужных координат (X/Y) на коэффициент масштабирования по соответствующей оси (xk, ytk). Напишем класс для этой трансформации в 2D-пространстве:
Единичная матрица
Следующий код использует единичную матрицу – такую, у которой по диагонали единицы, а остальные элементы нулевые. Она не приводит ни к каким изменениям исходного изображения и нужна лишь в качестве основы для удобного построения матрицы трансформации.
Добавьте этот метод в класс Matrices.
Отражение
Третья трансформация, с которой мы будем работать – отражение. Она выполняется за счет изменения знака X и Y – соответственно вектор переворачивается по вертикальной или горизонтальной оси. Класс для применения этой матрицы выглядит так:
Преобразование цвета
Последняя трансформация, которую мы рассмотрим, – изменение цветовой плотности. Она предусматривает применение разных коэффициентов к компонентам цвета (красный, зеленый, синий и альфа-канал). Например, если требуется сделать изображение на 50% прозрачным, нужно умножить значение альфа-канала на 0.5. Чтобы удалить полностью красный цвет, нужно умножить его на 0, и так далее.
Собираем всё вместе
Теперь нужно лишь собрать из отдельных трансформаций целый инструмент. Создадим класс ImageTransformer, отвечающий за преобразование изображения и добаим в него следующие методы:
Мы начали с определения двух перегрузок функции Apply. Одна из них принимает первым параметром имя файла изображения, а другая – bitmap-объект. вторым аргументом передается список трансформаций, которые нужно применить.
Внутри Apply() преобразования разделяются на 2 группы:
манипулирующие положением точек (X и Y);
манипулирующие цветом.
Для каждой группы объединяем трансформации в единую матрицу с помощью функции CreateTransformationMatrix().
Затем сканируем изображение и применяем преобразования к точкам и цветам соответственно. Обратите внимание, мы добавили проверку на то, что преобразованные значения каналов находятся в допустимом диапазоне.
После применения трансформаций данные сохраняются в массиве для последующего использования.
В процессе сканирования мы отслеживаем минимальные и максимальные координаты. Это нужно, чтобы установить новый размер изображения, который может отличаться от исходного после применения изменений.
Наконец, создаем bitmap-объект с новыми данными изображения.
Создание клиента
Клиентская часть приложения очень простая. Вот так выглядит форма для ввода данных:
Взглянем на управляющий ей код:
Единственное, что следует отметить, – это хорошая практика вызова Dispose() на disposable-объектах для лучшей производительности.
Анализ производительности
В основном методе Multiply() вызов метода Array.GetLength() оказывает большое влияние на производительность. В этом можно убедиться, сравнив скорость выполнения кода с многократным вызовом GetLength и с кэшированием результатов единственного вызова – они отличаются почти в 2 раза!
Еще один способ повысить производительность Multiply() – использовать небезопасный код, получив прямой доступ к содержимому массива:
Небезопасный код не будет компилироваться, если вы не разрешите эту опцию в меню Проект -> Свойства -> Сборка.
***
Теперь вы можете запустить приложение и убедиться, что все операции трансформаций работают. Готовый код проекта можно найти в этом репозитории.