
Пару лет назад я написал очень простую реализацию фрактального сжатия изображений для студенческой работы и выложил код на github.
К моему удивлению, репозиторий оказался довольно популярным, поэтому я решил обновить код и написать статью, объясняющую его и теорию.
Теория
Эта часть довольно теоретическая, и если вас интересует только документация к коду, можете её пропустить.
Сжимающие отображения
Пусть — полное метрическое пространство, а — отображение из на .
Мы говорим, что является сжимающим отображением, если существует , такое, что:
Исходя из этого, будет обозначать сжимающее отображение с коэффициентом сжимания .
О сжимающих отображениях существует две важные теоремы: теорема Банаха о неподвижной точке и теорема коллажа.
Теорема о неподвижной точке: имеет уникальную неподвижную точку .
Для всех :
Следовательно, является последовательностью Коши, и является полным пространством, а значит, является сходящейся. Пусть её пределом будет .
Более того, поскольку сжимающее отображение является непрерывным как липшицево отображение, оно также является непрерывным, то есть . Следовательно, если стремится к бесконечности в , мы получаем . То есть является неподвижной точкой .
Мы показали, что имеет неподвижную точку. Давайте при помощи противоречия покажем, что она уникальна. Пусть — ещё одна неподвижная точка. Тогда:
Возникло противоречие.
Далее мы будем обозначать как неподвижную точку .
Теорема коллажа: если , то .
Если мы зафиксируем в , то получим .
При стремлении к бесконечности мы получим требуемый результат.
Вторая теорема говорит нам, что если мы найдём сжимающее отображение , такое, что близко к , то можем быть уверенными, что неподвижная точка тоже находится близко к .
Этот результат будет фундаментом для нашей дальнейшей работы. И в самом деле, вместо сохранения изображения нам достаточно сохранить сжимающее отображение, неподвижная точка которого близка к изображению.
Сжимающие отображения для изображений
В этой части я покажу, как создавать такие сжимающие отображения, чтобы неподвижная точка находилась близко к изображению.
Сначала давайте зададим множество изображения и расстояние. Мы выберем . — это множество матриц с строками, столбцами и с коэффициентами в интервале . Затем мы возьмём . — это расстояние, полученное из нормы Фробениуса.
Пусть — это изображение, которое мы хотим сжать.
Мы дважды разделим изображение на блоки:
- Сначала мы разделим изображение на конечные или интервальные блоки . Эти блоки разделены и покрывают изображение целиком.
- Затем мы разделяем изображение на блоки источников или доменов . Эти блоки необязательно разделены и необязательно покрывают всё изображение.
Например, мы можем сегментировать изображение следующим образом:

Затем для каждого блока интервала мы выбираем блок домена и отображение .
Далее мы можем определить функцию как:
Утверждение: если являются сжимающими отображениями, то и тоже сжимающее отображение.
Остаётся один вопрос, на который нужно ответить: как выбрать и ?
Теорема коллажа предлагает способ их выбора: если находится близко к для всех , то находится близко к и по теореме коллажа и тоже находятся близко.
Таким образом мы независимо для каждого можем построить множество сжимающих отображений из каждого на и выбрать наилучшее. В следующем разделе мы покажем все подробности этой операции.
Реализация
В каждый раздел я буду копировать интересные фрагменты кода, а весь скрипт можно найти здесь.
Разбиения
Я выбрал очень простой подход. Блоки источников и конечные блоки сегментируют изображение по сетке, как показано на изображении выше.
Размер блоков равен степеням двойки и это очень упрощает работу. Блоки источников имеют размер 8 на 8, а конечные блоки — 4 на 4.
Существуют и более сложные схемы разбиения. Например, мы можем использовать дерево квадрантов (quadtree), чтобы сильнее разбивать области с большим количеством деталей.
Преобразования
В этом разделе я покажу, как создавать сжимающие отображения из на .
Помните, что мы хотим сгенерировать такое отображение , чтобы было близко к . То есть чем больше отображений мы сгенерируем, тем больше вероятность найти хорошее.
Однако качество сжатия зависит от количества битов, необходимых для сохранения . То есть если множество функций будет слишком большим, то сжатие окажется плохим. Здесь нужно искать компромисс.
Я решил, что будет иметь следующий вид:
где — это функция для перехода от блоков 8 на 8 к блокам 4 на 4, и — аффинные преобразования, изменяет контрастность, а — яркость.
Функция reduce снижает размер изображения, усредняя окрестности:
def reduce(img, factor): result = np.zeros((img.shape[0] // factor, img.shape[1] // factor)) for i in range(result.shape[0]): for j in range(result.shape[1]): result[i,j] = np.mean(img[i*factor:(i+1)*factor,j*factor:(j+1)*factor]) return result
Функция rotate поворачивает изображение на заданный угол:
def rotate(img, angle): return ndimage.rotate(img, angle, reshape=False)
Для сохранения формы изображения угол может принимать только значения .
Функция flip отражает изображение зеркально, если direction равно -1 и не отражает, если значение равно 1:
def flip(img, direction): return img[::direction,:]
Полное преобразование выполняется функцией apply_transformation:
def apply_transformation(img, direction, angle, contrast=1.0, brightness=0.0): return contrast*rotate(flip(img, direction), angle) + brightness
Нам нужен 1 бит, чтобы запомнить, требуется ли зеркальное отражение, и 2 бита для угла поворота. Более того, если мы сохраним и , использовав по 8 бит на каждую величину, то для сохранения преобразования нам в целом понадобится всего 11 битов.
Кроме того, нам следует проверять, являются ли эти функции сжимающими отображениями. Доказательство этого немного скучно, и не особо нам нужно. Возможно, позже я добавлю его как приложение к статье.
Сжатие
Алгоритм сжатия прост. Сначала мы генерируем все возможные аффинные преобразования всех блоков источников при помощи функции generate_all_transformed_blocks:
def generate_all_transformed_blocks(img, source_size, destination_size, step): factor = source_size // destination_size transformed_blocks = [] for k in range((img.shape[0] - source_size) // step + 1): for l in range((img.shape[1] - source_size) // step + 1): # Extract the source block and reduce it to the shape of a destination block S = reduce(img[k*step:k*step+source_size,l*step:l*step+source_size], factor) # Generate all possible transformed blocks for direction, angle in candidates: transformed_blocks.append((k, l, direction, angle, apply_transform(S, direction, angle))) return transformed_blocks
Затем для каждого конечного блока мы проверяем все ранее сгенерированные преобразованные блоки источников. Для каждого мы оптимизируем контрастность и яркость с помощью метода find_contrast_and_brightness2, и если протестированное преобразование наилучшее из всех пока найденных, то сохраняем его:
def compress(img, source_size, destination_size, step): transformations = [] transformed_blocks = generate_all_transformed_blocks(img, source_size, destination_size, step) for i in range(img.shape[0] // destination_size): transformations.append([]) for j in range(img.shape[1] // destination_size): print(i, j) transformations[i].append(None) min_d = float('inf') # Extract the destination block D = img[i*destination_size:(i+1)*destination_size,j*destination_size:(j+1)*destination_size] # Test all possible transformations and take the best one for k, l, direction, angle, S in transformed_blocks: contrast, brightness = find_contrast_and_brightness2(D, S) S = contrast*S + brightness d = np.sum(np.square(D - S)) if d < min_d: min_d = d transformations[i][j] = (k, l, direction, angle, contrast, brightness) return transformations
Для нахождения наилучшей контрастности и яркости метод find_contrast_and_brightness2 просто решает задачу наименьших квадратов:
def find_contrast_and_brightness2(D, S): # Fit the contrast and the brightness A = np.concatenate((np.ones((S.size, 1)), np.reshape(S, (S.size, 1))), axis=1) b = np.reshape(D, (D.size,)) x, _, _, _ = np.linalg.lstsq(A, b) return x[1], x[0]
Распаковка
Алгоритм распаковки ещё проще. Мы начинаем с полностью случайного изображения, а затем несколько раз применяем сжимающее отображение :
def decompress(transformations, source_size, destination_size, step, nb_iter=8): factor = source_size // destination_size height = len(transformations) * destination_size width = len(transformations[0]) * destination_size iterations = [np.random.randint(0, 256, (height, width))] cur_img = np.zeros((height, width)) for i_iter in range(nb_iter): print(i_iter) for i in range(len(transformations)): for j in range(len(transformations[i])): # Apply transform k, l, flip, angle, contrast, brightness = transformations[i][j] S = reduce(iterations[-1][k*step:k*step+source_size,l*step:l*step+source_size], factor) D = apply_transformation(S, flip, angle, contrast, brightness) cur_img[i*destination_size:(i+1)*destination_size,j*destination_size:(j+1)*destination_size] = D iterations.append(cur_img) cur_img = np.zeros((height, width)) return iterations
Этот алгоритм срабатывает, потому что сжимающее отображение имеет уникальную неподвижную точку, и какое бы исходное изображение мы ни выбрали, мы будем стремиться к нему.
Думаю, настало время для небольшого примера. Я попробую сжать и распаковать изображение обезьяны:

Функция test_greyscale загружает изображение, сжимает его, распаковывает и показывает каждую итерацию распаковки:

Совсем неплохо для такой простой реализации!
RGB-изображения
Очень наивный подход к сжатию RGB-изображений заключается в сжатии всех трёх каналов по отдельности:
def compress_rgb(img, source_size, destination_size, step): img_r, img_g, img_b = extract_rgb(img) return [compress(img_r, source_size, destination_size, step), \ compress(img_g, source_size, destination_size, step), \ compress(img_b, source_size, destination_size, step)]
А для распаковки мы просто распаковываем по отдельности данные трёх каналов и собираем их в три канала изображения:
def decompress_rgb(transformations, source_size, destination_size, step, nb_iter=8): img_r = decompress(transformations[0], source_size, destination_size, step, nb_iter)[-1] img_g = decompress(transformations[1], source_size, destination_size, step, nb_iter)[-1] img_b = decompress(transformations[2], source_size, destination_size, step, nb_iter)[-1] return assemble_rbg(img_r, img_g, img_b)
Ещё одно очень простое решение заключается в использовании для всех трёх каналов одинакового сжимающего отображения, потому что часто они очень похожи.
Если хотите проверить, как это работает, то запустите функцию test_rgb:

Появились артефакты. Вероятно, этот метод слишком наивен для создания хороших результатов.
Куда двигаться дальше
Если вы хотите больше узнать о фрактальном сжатии изображений, то могу порекомендовать вам прочитать статью Fractal and Wavelet Image Compression Techniques Стивена Уэлстеда. Её легко читать и в ней объяснены более сложные техники.
ссылка на оригинал статьи https://habr.com/ru/post/479200/
Добавить комментарий