вот этот сайт и @okdoc.
Если кратко, то можно запихнуть в qr код любую картинку.
Я же решил пойти дальше и сделать qr видео.
Об этом и будет мой рассказ.
Идея довольно проста:
Сначала я решил написать рендер и нарезжщик видео на картинки.
Если рендер я уже ранее реализовывал, то с нарезщиком нужно было делать.
Давайте сразу к коду
import subprocess as sp
import io
from PIL import Image
import numpy as np
# Наше рендер
class Render:
def __init__(self, fps, size, output):
self.cmd_out = [
# будем использовать ffmpeg
'ffmpeg',
# нам не нужен банер ffmpeg в stdout
'-hide_banner',
# будем использовать pipe с raw картинкой на 24 бита
'-f', 'image2pipe', '-pix_fmt', 'rgb24',
# обязательно укажем fps видео
'-r', str(fps),
# и пайп
'-i', '-',
# будем перемасщтабировать видео
'-vf', f'scale={size[0]}:{size[1]}',
# с методом скейлинга по соседям
'-sws_flags', 'neighbor',
# и укажем выходной формат видео
'-c:v', 'libx264', '-pix_fmt', 'yuv420p',
# перезапишем выходной файл
'-y',
# вот этот
str(output)
]
# нам нужен пайп куда будем писать данные
self.pipe = sp.Popen(self.cmd_out, stdin=sp.PIPE)
def push(self, qr):
# qr сразу в пайп
qr.png(self.pipe.stdin)
# и закрытие пайпа при завершении работы
def __del__(self):
self.pipe.stdin.close()
self.pipe.wait()
if self.pipe.returncode != 0:
raise sp.CalledProcessError(self.pipe.returncode, self.cmd_out)
# наш нарезщик
class Images:
def __init__(self, size, input):
self.cmd_out = [
# см. выше
'ffmpeg',
'-hide_banner',
# будем выводить только ошибки
'-loglevel', 'panic',
# входное видео
'-i', input,
# кропаем по высоте и уменьшаем размер картинки
'-vf', f'crop=in_h:in_h,scale={size}:{size}',
# выходной формат изображения в пайпе
'-c:v', 'rawvideo', '-f', 'image2pipe', '-pix_fmt', 'rgb24', '-'
]
self.size = (size, size)
# обрати внимание что здесь stdout, т.к. мы читаем на не пишем
self.pipe = sp.Popen(self.cmd_out, stdout=sp.PIPE)
# будем использовать итератор
def __iter__(self):
return self
def __next__(self):
# читаем сырой кадр
data = self.pipe.stdout.read(self.size[0] * self.size[1] * 3)
# преобразуем в картинку
return Image.frombuffer('RGB', self.size, data, 'raw', 'RGB', 0, 1)
# см. выше
def __del__(self):
self.pipe.stdout.close()
self.pipe.wait()
if self.pipe.returncode != 0:
raise sp.CalledProcessError(self.pipe.returncode, self.cmd_out)
Теперь можно взять и подготовить готовую библиотеку и файлы qrmap.py и tables.py.
Собираем всё в папку и пишем главный файл
from pathlib import Path
from PIL import Image
import numpy as np
import argparse
import io
import ffmpeg
import qrmap
if __name__ == '__main__':
# парсинг аргументов
parser = argparse.ArgumentParser(description='video as qrcode')
parser.add_argument('-i', dest='input', metavar='input', type=str, required=True, help='input video')
parser.add_argument('-f', dest='frame', metavar='frame', type=int, default=30, help='frames per second (default: 30)')
parser.add_argument('-s', dest='scale', metavar='scale', type=int, default=4, help='scale factor (default: 4)')
parser.add_argument('-u', dest='url', metavar='url', type=str, default='some text', help='qr code data')
parser.add_argument('-q', dest='qr_size', metavar='qr_size', type=int, default=177, help='qr size')
parser.add_argument('-o', dest='output', metavar='output', type=str, default='render.mp4',
help='output video file (default: render.mp4')
args = parser.parse_args()
if args.scale < 1:
print(f'[error]: scale factor cannot be less than 1')
exit()
# размер выходного видео
new_size = (args.qr_size * args.scale, args.qr_size * args.scale)
# инициализируем рендер
render = ffmpeg.Render(fps=args.frame, size=new_size, output=args.output)
# и итерируемся по кадрам входного видео
for index, frame in enumerate(ffmpeg.Images(input=args.input, size=args.qr_size)):
# применяем дизеринг ('1') и преобразуем в палитру в градации серого ('L'),
# а также поворачиваем картинку из-за массивов numpy
frame = frame.convert('1').convert('L').rotate(90)
# преобразуем массив в нужный вид
data = np.array(frame).reshape((frame.size[1], frame.size[0], 1))
# генеруем QR код
design = qrmap.QrMap.from_array(data)
qr = qrmap.create_qr_from_map(design, args.url, 'binary', 'L')
# и отправляем его в рендер
render.push(qr)
print(f'\rframe {index}', end='')
Результат работы (с добавленной звуковой дорожкой) можно посмотреть здесь, а архив со всем проектом доступен здесь.
А на этом пока всё, всем пока!