Найти в Дзене
ZDG

Функция переворота изображения

Оглавление

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

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

Задача переворота шире и универсальнее, чем OpenGL, поэтому я вынес её в отдельную тему.

Картинка это двумерный массив, но её также можно представить как массив строк:

Чтобы перевернуть картинку, надо поменять местами именно целые строки:

-2

Можно заметить, что меняются пары строк, которые расположены на равном расстоянии от середины. Если в картинке нечётное количество строк, то посередине остаётся строка, которая ни с кем не меняется.

Устройство SDL-поверхности

Мы пользуемся кроссплатформенной графической библиотекой SDL2. Загруженная из файла картинка становится поверхностью (тип SDL_Surface), от которой мы можем получить следующие свойства:

  • pixels – указатель на массив пикселов картинки, то есть это и есть тот самый массив, о котором речь шла выше
  • w – ширина картинки в пикселах
  • h – высота картинки в пикселах
  • BytesPerPixel – размер пиксела в байтах
  • pitch – длина строки в байтах

Обратите внимание, что если умножить размер пиксела в байтах (BytesPerPixel) на ширину картинки (w), мы по идее должны получить длину строки в байтах (pitch). Но это не всегда так. Длина строки может дополнительно выравниваться до границы 4-х байт, поэтому, если мы собираемся копировать строки, руководствоваться надо именно значением pitch.

Общий алгоритм

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

Очевидно, самая первая строка начинается там же, где начинается сам массив. У этой строки смещение 0 от начала массива. Следующая строка начинается через pitch байт от начала предыдущей. Значит, у неё смещение pitch от начала массива. У следующей строки смещение 2*pitch, затем 3*pitch, и т.д.

-3

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

Для строки со смещением 0 парной будет строка со смещением (h - 1) * pitch, для строки со смещением pitch парной будет (h - 2) * pitch, и т.д.

Для повторений в цикле заведём переменную y. Тогда пара смещений строк будет получаться так:

pixels[y * pitch]
pixels[(h - 1 - y) * pitch]

Сама переменная y должна меняться от 0 до h/2. То есть цикл будет доходить только до середины картинки. Парные смещения строк сойдутся там же.

-4

Теперь нужен ещё один цикл, чтобы пройтись уже по каждому байту внутри строки.

-5

Двигаясь по x от 0 до pitch (т.е. это дополнительное смещение относительно смещения по y), мы взаимно меняем байты в двух строках.

Собственно задача решена, и это самое базовое решение.

Что можно улучшить?

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

Но чисто ради упражнения можно кое-что изменить.

Во-первых, можно избавиться от побайтного копирования. Мы можем пересылать данные сразу по 4 байта, используя тип long int:

-6

Обратим внимание, что до этого pixels был байтовым указателем, а теперь он 4-байтовый. Данные не поменялись, поменялся только тип указателя на них. Если раньше размер элемента по указателю был 1 байт, то теперь он равен 4 байта. И все смещения вида pixels[x] теперь автоматически умножаются на 4. Соответственно pitch в байтах мы делим на 4, чтобы получить размер строки в 4-байтовых элементах.

Наконец, можно использовать функцию memcpy():

-7

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

Правда, для копирования строки целиком требуется временный буфер памяти размером в одну строку. Мы его динамически выделяем с помощью malloc(), а затем освобождаем с помощью free().

На что ещё тут можно обратить внимание:

Мы не используем привычный цикл по y и не вычисляем в нём смещения строк. Мы заранее приготовили два указателя: для первой и последней строки. Затем, после копирования, к указателю первой строки прибавляется pitch (то есть получаем следующую строку), а из указателя последней опять же вычитается pitch (то есть получаем предыдущую). Эти два указателя сходятся друг к другу, и мы повторяем итерации до тех пор, пока верхний указатель меньше нижнего.