Как стать автором
Обновить

Составляем красивый коллаж картинок

Так получилось, что мне давно понравилась выдача картинок Вконтакте, но я долгое время не обращал на них внимания, считая чем-то рядовым. Настолько они были гармоничными и приятные глазу, что казались чем-то естественным и простым.

Коллаж на python

Решено было повторить генерацию коллажа для картинок товаров интернет-магазина. Оказалось, что существует множество решений на JQuery, но загружать клиентскую машину лишней работой уж больно не хотелось. Было желание произвести подготовку коллажа на стороне сервера. Причём, не с фиксированной высотой строки как у Яндекс.картинок, а именно с полной динамикой как по вертикали, так и по горизонтали.

Поиски огорчили, готового решения на python нет. Но есть библиотека Photocollage, работающая лишь на gui и, по-моему, не на всех даже платформах. Не долго думав, мной был заимствован единственный его ключевой модуль collage.py), содержащий все объекты, всю математику расчёта нужного мне коллажа и невообразимо удобный API с доступом ко всем параметрам каждого элемента сгенерированного коллажа. Для его работы требуется лишь установленный PIL. И нужен он только для снятия ширины и высоты исходных изображений при создании объекта Photo.

На основании класса UserCollage, передав ему просто список полных адресов картинок на сервере, мы получаем объект-прототип коллажа new_collage.

И всё бы просто, надо лишь вывести этот коллаж в html, но я быстро столкнулся с парой проблем:
• Оказалось, что коллаж генерируется под горизонтальный рост. Т.е. каждая 3я картинка генерировала дополнительный столбец. Что не может устраивать, если ширина коллажа строго ограничена и его рост должен идти вниз.
• Объект new_collage просто не предусматривает отступы между картинками. Исходный модуль делает это при отрисовке.
Первая проблема решилась быстро, переписав расчёт в UserCollage и поделив коллажи на горизонтальные и вертикальные. Вызывая метод make_page, назначаем критичную ширину коллажа, получаем готовый объект new_collage.page. С ним и работаем.

class UserCollage(object):
    """UserCollage получает список фотографий и подбирает количество колонок в коллаже.
    Задавая collage_type можно задать тип коллажа: вертикальный или горизонтальный. 
    При горизонтальном коллаже количество колонок не ограничено, что недопустимо при вертикальном.
    Вертикальный коллаж ограничен 3 колонками.
    """
    def __init__(self, filelist, collage_type = "horizontal", no_cols=None):
        self.photolist = self.build_photolist(filelist)
        self.collage_type = collage_type
        if not no_cols:
            if self.collage_type == "horizontal":
                no_cols = int(round(1.5 * math.sqrt(len(self.photolist))))
            elif self.collage_type == "vertical":
                no_cols = int(math.ceil(math.sqrt(len(self.photolist))))
                #no_cols = no_cols if no_cols<3 else 3
                no_cols = 3 if len(self.photolist)>3 else no_cols
            self.no_cols = no_cols or 1

    def build_photolist(self, filelist):
        '''
        Получаем список полных адресов картинок, собираем и возвращаем список объектов Photo
        '''
        ret = []

        for name in filelist:
            try:
                img = PIL.Image.open(name)
            except IOError:
                raise BadPhoto(name)
            w, h = img.size
            orientation = 0
            try:
                exif = img._getexif()
                if 274 in exif:  # orientation tag
                    orientation = exif[274]
                    if orientation == 6 or orientation == 8:
                        w, h = h, w
            except:
                pass
            ret.append(collage.Photo(name, w, h, orientation))
        return ret
        
    def make_page(self, target_w=300):
        '''
        Собирает и возвращает объект Page. Это протитип коллажа. 
        Метод adjust финализирует коллаж, убирая пустоты и, 
        выравнивая столбцы Collumn и ячейки Cell по высоте и ширине соот-но.
        Можно задать целевую ширину target_w, но не факт, что коллаж получится не меньше.
        '''
        self.page = Page(target_w, self.no_cols)
        for num, photo in enumerate(self.photolist):
            if num==0:
                col = self.page.next_free_col()
                left = col.left_neighbor()
                right = col.right_neighbor()
                if left and abs(col.h - left.h) < 0.5 * col.w:
                    self.page.add_cell_multi_col(left, col, photo)
                elif right and abs(col.h - right.h) < 0.5 * col.w:
                    self.page.add_cell_multi_col(col, right, photo)
                else: self.page.add_cell(photo)
            else: self.page.add_cell(photo)


Вторую проблему пришлось решать при составлении html коллажа. Решено было назначить каждой ячейке style=”position:absolute”, передать ей координаты left и top, значение border отнять от её ширины и высоты, кроме последней. Также не забываем, в случае, если картинка больше ячейки, отцентровать её по сердцевине. Итог получился таким:

class MyPage ( collage.Page ):
    def to_html(self):
        '''Возвращает html-представление коллажа'''
        n = 0
        border = 10
        end = False
        div = ['<div style="width:287px;"><div style="height:%dpx;display:block;position:relative;overflow:hidden;">' % self.h]
        img={}
        while not end:
            end = True
            for no_col,col in enumerate(self.cols):

                if n < len(col.cells):
                    last_column = bool(no_col == len(self.cols)-1)
                        
                    if not col.cells[n].photo.filename in img:
                        img[col.cells[n].photo.filename] = {'width':0}
                    
                    if col.cells[n].is_extension():
                        img[col.cells[n].photo.filename]['width']+=col.w
                    else:
                        margin_left = 0
                        margin_top  = 0
                        y = col.cells[n].y # абсолютная координата Y
                        x = col.x # абсолютная координата X
                        if not last_column:
                            if col.cells[n].photo.w-col.cells[n].w>0:
                                margin_left = -int((col.cells[n].photo.w - col.cells[n].w)/2)
                            if col.cells[n].photo.h-col.cells[n].h>0:
                                margin_top  = -int((col.cells[n].photo.h - col.cells[n].h)/2)
                        

                        img[col.cells[n].photo.filename]={
                            'width':img[col.cells[n].photo.filename]['width'] + col.w - border/2,
                            'img_width': col.cells[n].photo.w,
                            'height':col.cells[n].h - border/2,
                            'x':x,
                            'y':y,
                            'margin_left':margin_left if last_column else 0,
                            'margin_top' :margin_top}
                    if last_column:
                        img[col.cells[n].photo.filename]['width'] = ''
                            
                    #print n,no_col, img[col.cells[n].photo.filename]['width']
                    if n < len(col.cells) - 1:
                        end = False
            n += 1

        div.append('\n'.join(['''<a style="margin:5px;overflow:hidden;float:left;position:absolute;
        width:{width}px;height:{height}px;top:{top}px;left:{left}px;">
        <img src="{img}" style="margin-left: {margin_left}px;margin-top:{margin_top}px;width:{img_width}px;"></a>'''.format( 
                    width       = attrs['width']      ,
                    img_width   = attrs['img_width']  ,
                    height      = attrs['height']     ,
                    top         = attrs['y']          ,
                    left        = attrs['x']          ,
                    margin_left = attrs['margin_left'],
                    margin_top  = attrs['margin_top'] ,
                    img         = src) for src,attrs in img.items()]))
        div.append('</div></div>')
        return ''.join(div)


Данный способ создания коллажа имеет недостатки:
• В нём присутствует рандом. Т.е. невозможно точно предугадать какой коллаж мы получим на выходе. Готового шаблона нет.
• Иногда не получаются коллажи идеально ровной формы.
• Объединение по ширине одной ячейки максимум двух колонок.

Итоговый код:

# -*- coding: UTF-8 -*-

import collage, os, math
import PIL.Image
target_w=287 #Целевая ширина


class MyPage ( collage.Page ):
    def to_html(self):
        '''Возвращает html-представление коллажа'''
        n = 0
        border = 10
        end = False
        div = ['<div style="width:287px;"><div style="height:%dpx;display:block;position:relative;overflow:hidden;">' % self.h]
        img={}
        while not end:
            end = True
            for no_col,col in enumerate(self.cols):

                if n < len(col.cells):
                    last_column = bool(no_col == len(self.cols)-1)
                        
                    if not col.cells[n].photo.filename in img:
                        img[col.cells[n].photo.filename] = {'width':0}
                    
                    if col.cells[n].is_extension():
                        img[col.cells[n].photo.filename]['width']+=col.w
                    else:
                        margin_left = 0
                        margin_top  = 0
                        y = col.cells[n].y # абсолютная координата Y
                        x = col.x # абсолютная координата X
                        if not last_column:
                            if col.cells[n].photo.w-col.cells[n].w>0:
                                margin_left = -int((col.cells[n].photo.w - col.cells[n].w)/2)
                            if col.cells[n].photo.h-col.cells[n].h>0:
                                margin_top  = -int((col.cells[n].photo.h - col.cells[n].h)/2)
                        

                        img[col.cells[n].photo.filename]={
                            'width':img[col.cells[n].photo.filename]['width'] + col.w - border/2,
                            'img_width': col.cells[n].photo.w,
                            'height':col.cells[n].h - border/2,
                            'x':x,
                            'y':y,
                            'margin_left':margin_left if last_column else 0,
                            'margin_top' :margin_top}
                    if last_column:
                        img[col.cells[n].photo.filename]['width'] = ''
                            
                    #print n,no_col, img[col.cells[n].photo.filename]['width']
                    if n < len(col.cells) - 1:
                        end = False
            n += 1

        div.append('\n'.join(['''<a style="margin:5px;overflow:hidden;float:left;position:absolute;
        width:{width}px;height:{height}px;top:{top}px;left:{left}px;">
        <img src="{img}" style="margin-left: {margin_left}px;margin-top:{margin_top}px;width:{img_width}px;"></a>'''.format( 
                    width       = attrs['width']      ,
                    img_width   = attrs['img_width']  ,
                    height      = attrs['height']     ,
                    top         = attrs['y']          ,
                    left        = attrs['x']          ,
                    margin_left = attrs['margin_left'],
                    margin_top  = attrs['margin_top'] ,
                    img         = src) for src,attrs in img.items()]))
        div.append('</div></div>')
        return ''.join(div)

    def adjust_cols_heights(self):
        """Set all columns' heights to same value by shrinking them"""
        
        all_heights = list(set([c.h for c in self.cols]))
        all_heights.sort()
        sum_w = sum(c.w for c in self.cols)
        while target_w*0.9>sum_w>target_w or all_heights:
            target_h = all_heights.pop()
            for c in self.cols:
                c.adjust_height(target_h)
        
class UserCollage(object):
    """UserCollage получает список фотографий и подбирает количество колонок в коллаже.
    Задавая collage_type можно задать тип коллажа: вертикальный или горизонтальный. 
    При горизонтальном коллаже количество колонок не ограничено, что недопустимо при вертикальном.
    Вертикальный коллаж ограничен 3 колонками.
    """
    def __init__(self, filelist, collage_type = "horizontal", no_cols=None):
        self.photolist = self.build_photolist(filelist)
        self.collage_type = collage_type
        if not no_cols:
            if self.collage_type == "horizontal":
                no_cols = int(round(1.5 * math.sqrt(len(self.photolist))))
            elif self.collage_type == "vertical":
                no_cols = int(math.ceil(math.sqrt(len(self.photolist))))
                #no_cols = no_cols if no_cols<3 else 3
                no_cols = 3 if len(self.photolist)>3 else no_cols
            self.no_cols = no_cols or 1

    def build_photolist(self, filelist):
        '''
        Получаем список полных адресов картинок, собираем и возвращаем список объектов Photo
        '''
        ret = []

        for name in filelist:
            try:
                img = PIL.Image.open(name)
            except IOError:
                raise BadPhoto(name)
            w, h = img.size
            orientation = 0
            try:
                exif = img._getexif()
                if 274 in exif:  # orientation tag
                    orientation = exif[274]
                    if orientation == 6 or orientation == 8:
                        w, h = h, w
            except:
                pass
            ret.append(collage.Photo(name, w, h, orientation))
        return ret
        
    def make_page(self, target_w=300):
        '''
        Собирает и возвращает объект Page. Это протитип коллажа. 
        Метод adjust финализирует коллаж, убирая пустоты и, 
        выравнивая столбцы Collumn и ячейки Cell по высоте и ширине соот-но.
        Можно задать целевую ширину target_w, но не факт, что коллаж получится не меньше.
        '''
        self.page = MyPage(target_w, self.no_cols)
        for num, photo in enumerate(self.photolist):
            if num==0:
                col = self.page.next_free_col()
                left = col.left_neighbor()
                right = col.right_neighbor()
                if left and abs(col.h - left.h) < 0.5 * col.w:
                    self.page.add_cell_multi_col(left, col, photo)
                elif right and abs(col.h - right.h) < 0.5 * col.w:
                    self.page.add_cell_multi_col(col, right, photo)
                else: self.page.add_cell(photo)
            else: self.page.add_cell(photo)
        
        
if __name__=="__main__":
    dir = 'd:\\' # Папка с фотками
    img_list = os.listdir(dir)
    new_collage = UserCollage([dir+x for x in img_list], collage_type = "vertical")
    new_collage.make_page(target_w)
    new_collage.page.adjust()

    print new_collage.page.to_html() # получаем HTML


Пример здесь. Можно обновлять страницу и посмотреть что предложит код. Если коллаж не впишется в target_w, будет выдана старая версия превью картинок.

Спасибо за внимание!
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.