Сказ о том как я из QRов видео собирал

Всем привет.

Эту небольшую заметку меня всподвигнул написать вот этот сайт и @okdoc.

Если кратко, то можно запихнуть в qr код любую картинку.

Я же решил пойти дальше и сделать qr видео.

Об этом и будет мой рассказ.

Идея

Идея довольно проста:

  1. Получаем кадр из видео.
  2. Режем под нужный размер.
  3. Используем эффект дизеринга, чтобы получить двухцветную картинку.
  4. Генерируем из картинки QR код.
  5. Передаём QR код в рендер.
  6. Переходим к 1, если есть ещё кадры.

Пилим

Сначала я решил написать рендер и нарезжщик видео на картинки.

Если рендер я уже ранее реализовывал, то с нарезщиком нужно было делать.

Давайте сразу к коду

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='')

Заключение

Результат работы (с добавленной звуковой дорожкой) можно посмотреть здесь, а архив со всем проектом доступен здесь.

А на этом пока всё, всем пока!

Что почитать

  1. На чём основана работа
Назад