Создание огромной мозаики изображений с помощью pyvips

Я пытаюсь создать генератор мозаики изображений, используя pyvips. Таким образом, для данного изображения (далее называемого исходным) создайте новое, более крупное изображение, похожее на исходное, за исключением того, что каждый пиксель (или, что более реалистично, группы пикселей) заменен меньшими отдельными фрагментами изображения.
Меня нарисовали. к pyvips, потому что говорят, что он может обрабатывать огромные изображения и что он может обрабатывать изображения без необходимости полной загрузки их в память. Однако у меня возникла проблема с созданием пустой мозаики, чтобы затем заполнить ее изображениями фрагментов.
В приведенном ниже коде я пытаюсь соединить фрагменты вместе ряд за рядом, чтобы создать мозаику, но этот код, к сожалению, съедает мою оперативную память и всегда дает сбои.

import os
import pyvips
from os.path import join
from scipy.spatial import cKDTree

class Mosaic(object):

    def __init__(self, dir_path, original_path, tree=None, averages=None):
        self.dir_path = dir_path
        self.original = original_path
        self.tree = tree
        if averages:
            self.averages = averages
        else:
            self.averages = {}

    def get_image(self, path):
        return pyvips.Image.new_from_file(path, access="sequential")

    def build_tree(self):
        for root, dirs, files in os.walk(self.dir_path):
            print('Loading images from', root, '...')
            for file_name in files:
                path = join(root, file_name)
                try:
                    image = pyvips.Image.new_from_file(path)
                    self.averages[self.avg_rgb(image)] = path
                except pyvips.error.Error:
                    print('File', path, 'not recognized as an image.')
        self.tree = cKDTree(self.averages.keys())
        print('Loaded', len(self.averages), 'images.')

    def avg_rgb(self, image):
        m = image.stats()
        return tuple(m(4,i)[0] for i in range(1,4))

    def get_tile_name(self, patch):
        avg = self.avg_rgb(patch)
        index = self.tree.query(avg)[1]
        return self.averages[tuple(self.tree.data[index])]

    def get_tile(self, x, y, step):
        patch = self.get_image(self.original).crop(x, y, step, step)
        patch_name = self.get_tile_name(patch)
        return pyvips.Image.new_from_file(patch_name, access="sequential")

    def make_mosaic(self, tile_num, tile_size, mosaic_path):
        original = self.get_image(self.original)
        mosaic = None
        step = min(original.height, original.width) / tile_num
        for y in range(0, original.height, step):
            mosaic_row = None
            print('Building row', y/step, '/', original.height/step)
            for x in range(0, original.width, step):
                tile = self.get_tile(x, y, step)
                tile = tile.resize(float(tile_size) / float(min(tile.width, tile.height)))
                tile = tile.crop(0, 0, tile_size, tile_size)
                #mosaic.draw_image(tile, x, y)
                mosaic_row = tile if not mosaic_row else mosaic_row.join(tile, "horizontal")
            mosaic = mosaic_row if not mosaic else mosaic.join(mosaic_row, "vertical")
        mosaic.write_to_file(mosaic_path)

Я также пытался создать мозаику, изменив размер исходного изображения, а затем используя draw_image, как показано ниже, но это также дает сбой.

mosaic = self.get_image(self.original).resize(tile_size)

mosaic.draw_image(tile, x, y)

Наконец, я попытался создать мозаику из файла new_temp_file, и у меня возникли проблемы с записью во временное изображение.

Как заставить работать эту мозаичную программу?


person Max Smith    schedule 17.12.2017    source источник
comment
libvips использует рекурсивный алгоритм для определения того, какие пиксели вычислять дальше. Это означает, что если ваш конвейер изображений станет очень длинным, он может переполнить стек C и вызвать сбой. Взгляните на arrayjoin: это операция libvips, которая может объединять огромное количество изображений за один шаг. Я присоединился к более чем 10 000 сразу без проблем. jcupitt.github.io/libvips/API/current/   -  person jcupitt    schedule 18.12.2017
comment
Это хорошая идея, однако я не могу заставить ее работать, как в документах. Когда я вызываю arrayjoin для изображения, он не принимает никаких аргументов и, кажется, разделяет каждую полосу, создавая изображение в градациях серого для каждой полосы. то есть: я получаю 3 черно-белых изображения, которые соединены вместе.....   -  person Max Smith    schedule 21.12.2017
comment
Я постараюсь сделать вам образец программы в ближайшие несколько дней.   -  person jcupitt    schedule 24.12.2017
comment
Спасибо! моя цель сделать огромную мозаику для рождественского подарка! Счастливых праздников   -  person Max Smith    schedule 24.12.2017
comment
О боже :( мой ответ, наверное, запоздал. Все равно счастливых каникул.   -  person jcupitt    schedule 25.12.2017
comment
Я немного исправил свой ответ, теперь он должен быстро создавать огромные мозаики.   -  person jcupitt    schedule 25.12.2017


Ответы (1)


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

Самым простым решением было бы использовать arrayjoin. Это оператор libvips, который может объединять множество изображений одним вызовом:

http://jcupitt.github.io/libvips/API/current/libvips-conversion.html#vips-arrayjoin

На github libvips есть пример использования его для одновременного объединения 30 000 изображений:

https://github.com/jcupitt/libvips/issues/471

(хотя это использует предыдущую версию привязки libvips Python)

Я адаптировал вашу программу для использования arrayjoin и изменил способ загрузки изображений. Я заметил, что вы также перезагружали исходное изображение для каждой выходной плитки, поэтому удаление этого дало хорошее ускорение.

#!/usr/bin/python2

from __future__ import print_function
import os
import sys
import pyvips
from os.path import join
from scipy.spatial import cKDTree

class Mosaic(object):

    def __init__(self, dir_path, original_path, tile_size=128, tree=None, averages=None):
        self.dir_path = dir_path
        self.original_path = original_path
        self.tile_size = tile_size
        self.tree = tree
        if averages:
            self.averages = averages
        else:
            self.averages = {}

    def avg_rgb(self, image):
        m = image.stats()
        return tuple(m(4,i)[0] for i in range(1,4))

    def build_tree(self):
        for root, dirs, files in os.walk(self.dir_path):
            print('Loading images from', root, '...')
            for file_name in files:
                path = join(root, file_name)
                try:
                    # load image as a square image of size tile_size X tile_size
                    tile = pyvips.Image.thumbnail(path, self.tile_size,
                                                  height=self.tile_size,
                                                  crop='centre')
                    # render into memory
                    tile = tile.copy_memory()
                    self.averages[self.avg_rgb(tile)] = tile
                except pyvips.error.Error:
                    print('File', path, 'not recognized as an image.')
        self.tree = cKDTree(self.averages.keys())
        print('Loaded', len(self.averages), 'images.')

    def fetch_tree(self, patch):
        avg = self.avg_rgb(patch)
        index = self.tree.query(avg)[1]

        return self.averages[tuple(self.tree.data[index])]

    def make_mosaic(self, tile_num, mosaic_path):
        mosaic = None
        original = pyvips.Image.new_from_file(self.original_path)
        step = min(original.height, original.width) / tile_num
        tiles_across = original.width / step
        tiles_down = original.height / step
        tiles = []
        for y in range(0, tiles_down):
            print('Building row', y, '/', tiles_down)
            for x in range(0, tiles_across):
                patch = original.crop(x * step, y * step, 
                                      min(step, original.width - x * step), 
                                      min(step, original.height - y * step)) 
                tile = self.fetch_tree(patch) 
                tiles.append(tile)

        mosaic = pyvips.Image.arrayjoin(tiles, across=tiles_across)

        print('writing ', mosaic_path)
        mosaic.write_to_file(mosaic_path)

mosaic = Mosaic(sys.argv[1], sys.argv[2])
mosaic.build_tree()
mosaic.make_mosaic(200, sys.argv[3])

Я могу запустить это так:

$ time ./mosaic2.py samples/ k2.jpg x.png
Loading images from samples/ ...
Loaded 228 images.
Building row 0 / 292
...
Building row 291 / 292
writing  x.png
real    7m19.333s
user    7m27.322s
sys     0m30.578s

в этом случае создается изображение размером 26496 x 37376 пикселей, и оно занимает около 150 МБ памяти.

person jcupitt    schedule 25.12.2017
comment
С Рождеством, Джон! - person Mark Setchell; 25.12.2017
comment
Это выглядит потрясающе! Спасибо большое. Один быстрый вопрос, по какой-то причине я получаю VipsOperation: миниатюра класса не найдена, когда я пытаюсь запустить программу, хотя все правильно установлено и обновлено. На самом деле я даже получаю функциональный объект ‹function call_function по адресу 0x7f7f1e1c8320›, когда я вызываю pyvips.Image.thumbnail, но при попытке вызвать его происходит сбой. Любые идеи/вещи, на которые вы могли бы указать мне, чтобы исправить это? Спасибо за вашу помощь, я очень ценю это. Счастливого Рождества! - person Max Smith; 26.12.2017
comment
Я предполагаю, что у вас установлена ​​старая версия libvips -- thumbnail была добавлена ​​в 8.5. Вместо этого вы можете использовать new_from_file и resize, хотя это будет немного медленнее. Используйте sequential, когда открываете изображение для изменения размера. - person jcupitt; 26.12.2017