Кажется, есть алгоритм!
Идея пришла в этот понедельник вечером. Потом додумал и реализовал, чтобы протестить на больших картинках.
Исходник программы на Си во вложении (rotate.c). Вызов:
rotate src.ppm dest.ppm (для простоты читает и пишет только самый примитивный формат изображений - PPM).
Общая суть алгоритма такова. Все операции записи с массивом производятся при помощи только одной элементарной операции - swap каких-то двух пикселей, таким образом информация в принципе никуда никогда не теряется, а только перетасовывается внутри изображения. Все без исключения операции swap приводят к перемещению какого-то пикселя на своё окончательное место, так что больше он оттуда уже не уходит, а сидит там до самого конца. Таким образом, всего мы производим ровно width * height операций swap. Соответственно, при каждом swap'е элемент, который сидит на месте, на которое мы окончательно перемещаем очередной пиксель, перемещается в какой-то другой промежуточный для него неокончательный пиксель. Далее вся фишка алгоритма. Мы сначала перемещаем те пиксели, у которых конечное положение в массиве лежит позже начального, а затем те, у которых оно лежит раньше начального (то есть сначала те, которые глобально нужно переместить вперёд в массиве, а потом те, которые глобально нужно переместить назад в массиве). Делаем это очень просто. Берём пиксель, который нужно переместить и перемещаем его. Здесь именно два действия: берём и перемещаем. Причём первое намного более трудоёмкое. Мы знаем глобальное начальное положение пикселя и глобальное конечное положение. Перемещать будем swap'ом в глобальное конечное положение. А вот откуда мы будем его брать? Мы знаем, где оно находилось в самом начале алгоритма. Далее в процессе алгоритма действиями "перемещения в какой-то другой промежуточный для пикселя неокончательный пиксель" при swap'ах, ставящих на место совсем другие пиксели, он переместился в какое-то другое место. Мы просто берём и отслеживаем по шагам всю историю таких действий с этим пикселем. И выясняем, где он сейчас. В программе это реализуется функцией realPixelPosition(). С ним могли производится следующие операции. Сначала были постановки на место пикселей, глобально перемещаемых вперёд. На место нашего пикселя могли переместить какой-то другой пиксель. Но мы знаем, что это за пиксель. Для каждого пикселя мы знаем координату не только того места, в которое мы должны его переместить, но и то, откуда в него мы должны переместить. Оно тоже вычисляется по простой формуле. Таким образом, на первом этапе единственное, что могли сделать с нашим пикселем - это переместить в него известный нам пиксель. При этом наш пиксель переместился на место пикселя-жлоба. Но где был этот жлоб? Он глобально находится раньше нашего, потому что глобально перемещается вперёд. Мы знаем его начальное положение, но перед своим актом жлобства он мог переместиться. Он мог переместиться только во время первого этапа, потому что он перемещается глобально вперёд. При этом его мог свергнуть с места только какой-то другой жлоб, который тоже перемещается глобально вперёд и который тоже глобально находится раньше первого жлоба. И его начальное место мы тоже знаем (вычисляется по той же простой формуле, что и место первого жлоба, но только не относительно безвредного пикселя, а относительно первого жлоба). Короче, применяя эту простую формулу, мы будем двигаться назад до тех пор, пока не наткнёмся на пиксель, который никто на первом этапе не трогал (его мы определим по тому признаку, что в него пиксель перемещается глобально назад). Координата этого последнего пикселя и будет искомой координатой, в которую при swap'е попал наш пиксель. Далее отслеживается второй этап (где пиксели перемещаются глобально назад). На втором этапе пиксель могли сдвинуть только при перемещении какого-то пикселя глобально назад. Мы знаем начальную координату пикселя-циника, который сделал это. Она вычисляется по той же самой простой формуле. Причём сдвинуть его могли уже несколько раз, поскольку при сдвиганиях он перемещается вперёд. Мы вычисляем начальную координату первого циника. Если мы этого пикселя-циника уже переместили на место раньше, или мы его только будем ещё перемещать позже, то это перемещение реально не оказало влияния на наш пиксель, и он реально на втором этапе остался на месте, и текущая имеющаяся наша координата является ответом (местом, на котором реально находится пиксель). Если же указанное условие не выполнено, то перемещение пикселя произошло. Мы рекурсивно (да, да) вызываем функцию realPixelPosition() и находим реальную позицию пикселя-циника, на место которого переместился наш пиксель. Далее мы проверяем не сместил ли его кто другой далее таким же образом. В цикле (ура). Когда-нибудь мы придём в такое место, с которого его уже никто не смещал, так как мы двигались только вперёд. В обшем-то вот и весь алгоритм.
Насчёт рекурсии. Я с самого начал чувствовал, что она не будет глубоко заходить (максимальная глубина не будет зависеть от размера изображения при больших изображениях). При повышении размера изображения она медленно увеличивается, пока где-то в районе 20 килопикселей не достигает значения насыщения 20 (это гдё-то 0,5 килобайта памяти). Далее колеблется в районе 20-25 с редкими выбросами. Для квадратных изображений глубина рекурсии всегда 2 (видимо, тут что-то принципиальное). Я протестировал кучу изображений от 6 пикселей до 42 мегапикселей. Максимальное значение выброса было 44 на фотографии 3 мегапикселя (кстати, изображённой ниже). Таким образом, получается, что доп. память в среднем не зависит от размера изображения при больших изображениях.
Алгоритм работает примерно в 6 раз медленнее, чем алгоритм с созданием второго массива.
Есть алгоритм (rotate_back2.c), который поворачивает изображения приближённо, но кушает всегда константные 16 байт и работает по скорости почти как алгоритм с созданием второго массива. Приближённо здесь значит, что половина конечного изображения получается всегда правильно, а вторая половина замусорена, но общий смысл изображённого понять можно (~ 1 Мб трафика):
Википедия — это наилучший источник информации по теме «Википедия».