Так получилось, что фотография, это мой основной профессиональный вид деятельности, а программирование — хобби, которое иногда позволяет размять
Недавно поставил себе задачу, как бы еще порадовать своих клиентов. Вспомнил многочисленные просьбы клиентов на свадебную съемку: «Как хорошо было бы, если бы на банкете вы смогли показать коротенькое слайдшоу из фотографий, которые отсняли за день». На эти просьбы приходилось отказывать, по нескольким причинам: лень таскать с собой ноутбук для сборки слайдшоу, нет времени на отбор пары десятков снимков из сотен, из raw опять же нужно конвертировать, и самое главное — на это все нужно время, которого нет.
Это рассказ, о том, как мне удалось сделать для себя инструмент, который с минимальным моим участием и минимальным дополнительным весом в рюкзаке, помогает сделать красивые слайдшоу. И конечно же рассказ о python, ffmpeg и linux на android.
Неожиданный выбор железа
Первая проблема — это лишний вес. Мне нужен был полноценный linux на достаточно приличном железе. Изначально мой выбор пал на Orange PI PC, о котором я услышал на гигтаймсе. Железка была заказана и доставлена. Мне казалось, это то что нужно — 4 ядра по 1.5 ггц, 1 гб оперативной памяти и полноценные USB. Но на деле, лишний раз убедился, что без нормальной поддержки, все «клоны raspberry», ничего не стоят. Очень глючные образы OS, постоянно отваливающиеся ядра под нагрузкой, проблема с работой библиотек, чтоб, например подключить lcd дисплей.
И самое главная проблема, это неожиданный killed, при свободных 800 мб оперативки, на участке кода типа:
from PIL import Image
img=Image.new('RGB',(6000,4000)) #на деле мне нужно было не создавать, а открывать фотографии
img.rotate()
Причем тоже самое прекрасно работало на нетбуке с 1гб оперативке без свопа, а так же на Raspberry PI первом. И уж тем более, и речи не могло быть, чтоб делать тоже самое, но на 4-х ядрах одновременно.
Решение пришло неожиданно, когда я взял в руки смартфон, чтоб прочитать пришедшее сообщение:
А в кармане то постоянно лежит железка с 2.2 ггц 4-х ядерным процессором, 2гб оперативном памяти + USB-otg имеется (Nexus 5). Осталось найти способ полноценного запуска Linux окружения. После отбрасывания различных вариантов с перепрошивкой (хотелось пользоваться им полноценно и как смартфоном), выход был найден — Linux Deploy. Если кратко, Linux Deploy запускает полноценное linux окружение в chroot'e (подробнее о программе можно почитать в блоге у нашего соотечественника — разработчика), и самое главное для меня — монтировать произвольный каталог из fs android в свое окружение. Без этого, не была бы возможна работа с картридером SD карт памяти, воткнутым в OTG разъем.
Отбор фотографий
Слайдшоу из сотен фотографий, заняло бы пару часов времени. Нужен был способ легко и быстро отобрать 20-40 фотографий. Пролистывать даже 100 фотографий со смартфона — то еще удовольствие, а количество может доходить к вечеру до тысячи (дубли с серийной съемки, брак, пристрелочные фото, репортаж и пр.)
Взглянув на фотоаппарат, вспомнил про кнопку, которой никогда не пользовался — спасительницей оказалась кнопка «rate», которая присваивает рейтинг фотографии:
Колесом прокрутки справа достаточно быстро пролистываются снимки, и на нужном нажимается кнопка «rate». Так как ты уже знаешь, что удачного за сегодня отснял, на все уходит не больше пары минут. Остается заставить программу найти и выбрать те снимки, которым присвоен хоть какой нибудь рейтинг.
Так как рейтинг попадает в exif, понадобится замечательный пакет exiftool (sudo apt-get install libimage-exiftool-perl) и wrapper к нему для python. А дальше все просто:
import os
import exiftool
all_files=[]
"""проходимся по всем файлам на SD карте """
for directory, dirnames, filenames in os.walk(PATH_TO_SD_ROOT):
for name in filenames:
f=os.path.join(directory, name)
if f.lower().endswith('.cr2') or f.lower().endswith('.jpg'): #и добавляем в список jpg и raw файлы
all_files.append(f)
tool=exiftool.ExifTool()
tool.start() #запускаем exiftool
# просим пройтись по нашему списку и выдать рейтинги
result=tool.get_tags_batch(['XMP:Rating'],all_files)
rated_files=[]
for x in result:
if x['XMP:Rating']>0:
rated_files.append(x['SourceFile'])
# на выходе получаем список нужных нам файлов
rated_files.sort()
Следующий этап достаточно тривиален — копирование нужных фотографий во временный каталог и резайс в несколько потоков для дальнейшей работы. Единственное, на чем бы хотел заострить внимание, это raw, в который я снимаю. Конвертацией занимается утилита dcraw (хотя это не полноценная конвертация, а лишь выдергивание вшитой jpg в raw файл, но в данном случае, это более чем достаточно.
import subprocess
for n,x in enumerate(self.rated_files):
dcraw_opts = ["dcraw", "-e", "-c", x] # -e - вытащить вшитый jpg, -с выдать нам его в stdout
dcraw_proc = subprocess.Popen(dcraw_opts, stdout=subprocess.PIPE)
image = StringIO.StringIO(dcraw_proc.communicate()[0]) # берем фотографию со stdout
image.seek(0)
open('input/%02d.jpg'%(n),'wb').write(image.read()) # и записываем в нужное место.
Сделай мне красиво!
На предыдущем этапе можно было бы остановиться, взяв фотографии и пустив их как слайдшоу на ноутбуке диджея, подключенного к проектору, но хочется, чтоб все это выглядело красиво.
На помощь приходит такая замечательная вещь, как ffmpeg (avconv). Я не любитель каких либо ярких спецэффектов, мне достаточно легкой динамики, в виде zoom'a фотографии и «crossfade» перехода между слайдами. Скажу сразу, несмотря на огромные возможности ffmpeg, это сделать у меня не получилось. Например, фильтр zoompan, выдавал ужасное качество и дрожащую картинку. После недели, проведенные за чтением мануалов и форумов, решено было сделать это «в лоб»:
def processImage(numb):
img=Image.open('input_temp/%02d.jpg'%numb) # открываем текущее изображение
# для эффекта crossfade открывает следущее изображение, или если оно последнее, создаем пустое
try:next_img=Image.open('input_temp/%02d.jpg'%(numb+1)).resize((1280,720),Image.ANTIALIAS)
except:next_img=Image.new('RGB',(1280,720),'black')
# чтобы не захламлять память сотнями отдельных кадров, попросим ffmpeg принимать на stdin фотографии
# и получим на выходе готовый отрезок видео
p = subprocess.Popen(['avconv', '-y', '-f', 'image2pipe', '-vcodec', 'mjpeg', '-r', '25', '-i', '-', '-vcodec', 'mjpeg','-q:v', '3' , '-r', '25', 'output/%02d.mjpg'%(numb)], stdin=subprocess.PIPE)
# 100 кадров при 25 к/c - 4 секунды видео на слайд
for x in xrange(100):
# при каждой итерации делаем кроп исходной фотографии в соответствии с пропорцией 16:9
n=img.crop((int(float(x)*16.0/9.0),x,int(1920.0-float(x)*16.0/9.0),1080-x))
# и делаем резайс к конечному размеру
n=n.resize((1280,720),Image.ANTIALIAS)
# на третей секунде, начинаем "подмешивать" следующую фотграфию
if x>75:
n=Image.blend(n,next_img,float(x-75)/25)
# и скармливаем ffmpeg'у
n.save(p.stdin,'JPEG')
p.stdin.close()
p.wait()
Ах да, я что то там говорил про ядра процессора. Хотелось бы распаралеллить этот процесс, чтоб были заняты все ядра. В python это делается очень очень просто:
from multiprocessing import Pool
s=len(glob.glob('input_temp/*.jpg')) #берем количество фотографий
pool = Pool()
pool.map(processImage, xrange(s)) # и отдаем их воркерам, количество которых будет равно количеству ядер процессора
pool.close()
pool.join()
В итоге мы имеем множество отрезков mjpeg видео, которые нужно соединить воедино, вставив музыку.
Погуглив, не нашел лучшего способа, как сначала напрямую соединить видео, используя cat:
cat 00.mjpg 01.mjpg ..... > out.mjpg
Осталось только переконвертировать его в нужный формат, добавив музыку:
avconv -threads 4 -framerate 25 -i out.mjpg -i audio.mp3 -shortest -y -r 25 -preset veryfast out.mp4
Чтобы не возиться каждый раз в консоли, а нужно было выбирать музыкальный трек, вписывать название для слайдшоу (для первого кадра) и пр, поднял простой web сервер, который стартует при запуске Linux Deploy. Я использовал простенький фреймворк bottle. Выглядит это вот так:
Итого
2-3 минуты отбор фотографий, запуск Linux Deploy, localhost в браузере, пару секунд на то, чтоб вписать title и нажать на СТАРТ. Далее 10-15 минут работы смартфона, и видео готово:
Таким же способом можно делать не только слайдшоу из фотографий, но и склеивать видео: пометить нужные отрывки в камере кнопкой rate и склеить их потом ffmpeg'ом.
И небольшой анонс
Если эта тема окажется интересной, я сделаю еще несколько публикаций. Например, на очереди статья, как сделать вот такую милую и функциональную фотобудку:
Автор: kAIST