Catframes.py¶
Скрипт, который соединяет (англ. concatenate) кадры (англ. frames).
Сокращение cat в этом контексте знакомо любому пользователю Unix-подобных систем.
Примеры использования¶
В первом примере вы можете убедиться в нескольких вещах: стандартные настройки качества подойдут практически для всего, кадры с отличающимся соотношением сторон выравниваются по центру и цвет полей у этих кадров совпадает с цветом фона.
Анимация была отрендерена Блендером в серию PNG-изображений с прозрачным фоном, после чего первые несколько кадров обрезаны сверху и снизу, чтобы продемонстрировать поля.
catframes.py --margin-color='#a143ac' monkey/ monkey.webm
catframes.py --margin-color='#a143ac' monkey/ monkey.mp4
catframes.py --margin-color='#a143ac' -q high monkey/ monkey_hq.webm
catframes.py --margin-color='#a143ac' -q high monkey/ monkey_hq.mp4
catframes.py --margin-color='#a143ac' -q poor monkey/ monkey_pq.webm
catframes.py --margin-color='#a143ac' -q poor monkey/ monkey_pq.mp4

monkey_hq.webm (76M), monkey.webm (21M), monkey_pq.webm (9.2M)
monkey_hq.mp4 (61M), monkey.mp4 (22M), monkey_pq.mp4 (12M)
Стоп-кадры для сравнения качества:
monkey_hq.webm.png, monkey.webm.png, monkey_pq.webm.png
monkey_hq.mp4.png, monkey.mp4.png, monkey_pq.mp4.png
Во втором примере склеивается последовательность каталогов в заданном порядке, причем у содержимого этих каталогов отличаются разрешения, что отражено в названиях. Вы можете наблюдать, что положение надписей не зависит ни от разрешений, ни от соотношений сторон; надписи легко читаются как на светлом, так и на тёмном фоне; скрипт сам выбирает разрешение (в данном случае 1280x960).
catframes.py -r 60 \
--bottom='{catframes}\n' \
06_1280_720 \
07_1280_640 \
08_800_600 \
09_1024_768 \
10_640_480 \
11_720_1280 \
12_1440_1080 \
13_1440_1080 \
14_1280_960 \
15_1280_960 \
16_1280_960 \
17_800_600 \
18_800_600 \
19_800_600 \
mixed_resolutions.webm

mixed_resolutions.webm (113M), mixed_resolutions.mp4 (383M)
Перейдём ближе к реальной практике. Для примера я выставил из окна веб-камеру и направил её в небо, чтобы не вторгаться ни в чью частную жизнь. На компьютер круглосуточно сохранялись снимки. В реальной системе папки с ними пережимались бы по расписанию примерно следующей командой.
catframes.py \
--left-top='{dir}/{fn} (кадр {frame:video})\n\nПоследнее изменение: {mtime:%d.%m.%Y %H:%M:%S}\nРазмер файла, байты: {size}' \
--top='' --right-top='WARN' \
--right='{symlink:L}' \
--bottom-left='Архивировано {vtime:%d.%m.%Y} в {vtime:%H:%M} с помощью {catframes}.' \
/путь/к/кадрам/2022-08-27T00/ \
/путь/к/кадрам/2022-08-27T01/ \
/путь/к/кадрам/2022-08-27T02/ \
/путь/к/кадрам/2022-08-27T03/ \
/путь/к/кадрам/2022-08-27T04/ \
/путь/к/кадрам/2022-08-27T05/ \
/путь/к/кадрам/2022-08-27T06/ \
/путь/к/кадрам/2022-08-27T07/ \
/путь/к/кадрам/2022-08-27T08/ \
/путь/к/кадрам/2022-08-27T09/ \
/путь/к/кадрам/2022-08-27T10/ \
/путь/к/кадрам/2022-08-27T11/ \
/путь/к/кадрам/2022-08-27T12/ \
/путь/к/кадрам/2022-08-27T13/ \
/путь/к/кадрам/2022-08-27T14/ \
/путь/к/кадрам/2022-08-27T15/ \
/путь/к/кадрам/2022-08-27T16/ \
/путь/к/кадрам/2022-08-27T17/ \
/путь/к/кадрам/2022-08-27T18/ \
/путь/к/кадрам/2022-08-27T19/ \
/путь/к/кадрам/2022-08-27T20/ \
/путь/к/кадрам/2022-08-27T21/ \
/путь/к/кадрам/2022-08-27T22/ \
/путь/к/кадрам/2022-08-27T23/ \
/путь/к/архивам/2022-08-27.webm
Предупреждение
Скрипт завершается с ненулевым статусом, если какие-то папки с кадрами не существуют.
Я не настраивал у себя архивацию по расписанию, а выбрал интересный фрагмент и сжал его отдельно.
catframes.py \
--left-top='{dir}/{fn} (кадр {frame:video})\n\nПоследнее изменение: {mtime:%d.%m.%Y %H:%M:%S}\nРазмер файла, байты: {size}' \
--top='' --right-top='WARN' \
--right='{symlink:L}' \
--bottom-left='Архивировано {vtime:%d.%m.%Y} в {vtime:%H:%M} с помощью {catframes}.' \
2022-08-26T23/ \
2022-08-27T00/ \
2022-08-27T01/ \
2022-08-27T02/ \
2022-08-27.webm

2022-08-27.webm (13M)
Любые данные, оказывающиеся на периферии нашего внимания, чтобы быть по́нятыми, требуют встраивания в некоторый контекст, от которого зависит, как мы видим и формулируем факты. Видео — не исключение. Если описать это видео вне контекста, получится что-то вроде «на записи перемещается едва заметная точка».
Обратимся к напечатанной на кадрах метаинформации. В ней не сказано, где находится камера, но я сам недавно её ставил и помню, что она находилась в Казани и была направлена на юг и вверх примерно под 45 градусов. На кадрах зафиксировано время съёмки (имена файлов). Через эти отметки времени и горизонтальный угол обзора (около 45 градусов, я измерял) можно оценить угловую скорость, а с помощью перемотки и линейки, приложенной к экрану, — траекторию.
Нетрудно убедиться, что точка движется почти по идеальной прямой, и время прохода перед камерой равнялось примерно трём часам. Это исключает практически любые атмосферные объекты, включая распространённые виды летательных аппаратов, и всё, что находится на низких орбитах. Объект двигался с востока на запад, и с учётом скорости прохождения поля зрения, вероятно, он находился в дальнем космосе, а его движение на видео — иллюзия, вызванная вращением Земли.
Воспользуемся виртуальным планетарием, например https://stellarium-web.org/, укажем в нём время и место и убедимся, что как раз тогда на юге проходил Юпитер, один из самых ярких астрономических объектов.
Совет
Самая важная метаинформация у видео — время и место съёмки.
Пример также демонстрирует фиксацию подозрительных действий на сервере. Я нарочно
отредактировал три кадра, начиная с
2022-08-27-00-00-01.jpg
(см. время модификации),следующий отредактировал прямо во время архивирования (об этом сигнализирует предупреждение),
один кадр заменил симлинком (буква L справа),
один кадр во время архивирования удалил (красный экран),
один кадр во время архивирования запретил на чтение (красный экран),
один кадр запретил на чтение еще до запуска скрипта (красный экран).
Обратите внимание, сколько пришлось привнести сторонней информации для построения непротиворечивой интерпретационной модели, даже несмотря на эти индикаторы. Я апеллировал в основном к личному опыту, что не гарантирует достоверность и не всегда возможно. Вопрос критериев отбора такой информации остаётся открытым.
Пользовательский интерфейс¶
- class catframes.OverLang¶
Модуль разбора шаблонов оверлеев.
- classmethod compile(template: str) OverlayTemplate ¶
Оверлей может быть описан одним из двух способов.
Первый способ — написать только
WARN
и ничего больше (любым регистром). В этом случае, вся эта область будет показывать предупреждение о кадре, если оно есть.Второй способ — использовать произвольный текст с вычисляемыми выражениями в фигурных скобках вида
{[[выравнивание]ширина[!]:]функция[:аргумент]}
.Ширина — целое число больше нуля. Подразумевается минимальная ширина, но если добавить восклицательный знак, лишние символы будут отрезаны. Выравнивание может быть по левому краю (по-умолчанию), по правому краю (
>
) и приблизительно по центру (^
).Перечень доступных функций:
Функция
Описание
{catframes}
Название и версия скрипта.
{machine}
Значение
platform.machine()
. Обычно это архитектура процессора. Может быть пустой строкой, если не удаётся определить.{node}
Значение
platform.node()
. Сетевое имя компьютера. Может быть пустой строкой, если не удаётся определить.{vtime[:формат]}
Приблизительно соответствует времени запуска скрипта.
Синтаксис формата соответствует используемому в методе
datetime.datetime.strftime()
. Если не указывать, используется ISO 8601 с миллисекундами (обычно 23 символа).{fn}
Имя кадра целиком (не обязательно всегда указывать, поскольку предупреждения и ошибки обычно содержат имя файла).
{dir}
Название директории, где непосредственно находится текущий кадр.
{frame:контекст}
Номер кадра. Контекст определяет точку отсчёта.
Возможные значения:
dir
— текущая директория;dirs
— все директории;video
— видеозапись: нумерация начинается с единицы даже при использовании опции--trim-start
.
{mtime[:формат]}
Местное время последнего изменения кадра на диске. Пустая строка, если не получается определить.
Синтаксис формата соответствует используемому в методе
datetime.datetime.strftime()
. Если не указывать, используется ISO 8601 с миллисекундами.{size}
Размер кадра на диске в байтах.
{resolution}
Исходное разрешение кадра.
{symlink[:вид]}
Отметка о том, что кадр является симлинком или пустая строка. Вид по-умолчанию —
symlink
.
- class catframes.ConsoleInterface¶
Интерфейс пользователя.
Превращает аргументы командной строки в настройки и наборы данных. И тут также выводится информация в стандартный вывод по ходу анализа параметров скрипта.
- show_options()¶
Чтобы пользователь видел, как проинтерпретированы его аргументы.
- show_splitter()¶
Визуально отделить всё, что было выведено в консоль выше.
- get_input_sequence() Sequence[Frame] ¶
Возвращает отсортированную и пронумерованную последовательность кадров, которую попросил пользователь: параметры
--trim-start
и--trim-end
уже применены.- Исключение:
ValueError – не удалось прочитать список файлов или в указанных директориях нет ни одного изображения.
- get_output_options() OutputOptions ¶
Возвращает настройки сохранения видео.
- Исключение:
ValueError – пользователь указал файл с недопустимым расширением и т.п.
- property statistics_only: bool¶
Пользователь не хочет пока делать видео, только посмотреть логику выбора разрешения.
- static list_resolutions(resolutions: ResolutionStatistics, limit: int = 10)¶
Перечислить разрешения от самых частых к самым редким.
Сетевые возможности¶
Catframes использует протокол HTTP, чтобы передавать кадры в FFmpeg.
- catframes.WebApp¶
Первый аргумент — WSGI environment (включает всю информацию о запросе), второй — функция начала ответа, которая принимает статус HTTP-ответа и список HTTP-заголовков. В терминах MVC, это еще неразграниченные роутинг с контроллерами.
Подробнее читайте в официальной документации:
wsgiref.simple_server.make_server()
.
- catframes.WebJob¶
Функция, которая работает с локальным веб-приложением. Аргумент — номер порта.
- class catframes.JobServer(app: WebApp, job: WebJob)¶
Механизм межпроцессного взаимодействия на основе HTTP. Открывает порт и обслуживает другие программы, которые контролируются из этого же сервера.
Грубо говоря, эта штука позволяет делать что-то вроде динамической файловой системы, если считать URL файлами: в тот момент, когда что-то запрашивается сторонней программой, оно подготавливается на лету. Не тратится дисковое пространство, а если не использовать временные файлы при обработке запросов, то и ресурс SSD.
В целях безопасности:
на компьютере следует использовать файрвол;
лучше не передавать списки URL по HTTP-каналу.
Номер порта выбирается автоматически. Значения ниже десяти тысяч не используются, чтобы не мешать всяким Томкэтам и Ноджиэсам.
- Параметры:
app – Функция, которая обслуживает HTTP-запросы.
job – Запускается один раз. Сервер ждёт завершения. Выброшенные исключения приводят к остановке сервера и вылетают из метода
run()
.
Файловая система¶
Архивирование видео может запускаться автоматически и выполняться без присмотра. При этом нежелательно, чтобы сжатие данных за целый день аварийно остановилось, если что-то не получится сделать с одним кадром. Поэтому здесь реализованы обёртки над системными функциями для максимальной предсказуемости.
- class catframes.FileUtils¶
Модуль вспомогательных функций, связанных с файловой системой.
- static list_images(path: Path) List[Path] ¶
Набор файлов JPEG и PNG в порядке, предоставляемом pathlib (не определён в документации и, скорее всего, зависит от операционной системы). Файлы определяются по расширениям (суффиксам имён). Вложенные папки игнорируются.
- Исключение:
ValueError – путь не является директорией.
OSError – не удалось получить список файлов.
- static sort_natural(files: List[Path])¶
В Linux также известен как version sort. Многосимвольные десятичные числа считаются за один символ и сортируются в зависимости от значения числа.
Функция сортирует файлы или симлинки по именам, игнорируя их местоположения. Список сортируется на месте, чтобы экономить память.
Способ сортировки имён похож на используемый в команде sort из GNU Coreutils, если использовать её как
echo СПИСОК | sort -Vs
, но не совпадает полностью.
Основные сущности¶
- class catframes.Resolution(width: int, height: int)¶
Ненулевое разрешение в пикселях.
- __str__()¶
Возвращает строку вида
ШxВ
. Икс в качестве разделителя выбран из соображений совместимости с максимальным числом шрифтов оверлеев и кодировок терминалов.
- __eq__(other)¶
Return self==value.
- class catframes.Frame(path: Path)¶
Кадр на диске. Сырьё для
FrameView
. Конструктор создаёт объект, даже если путь ведёт в никуда. Не иммутабельная сущность, некоторые поля могут обновляться.- numvideo: int¶
Как
numdirs
, но если пользователь просит отрезать сколько-то кадров из начала последовательности, нумерация всё равно начинается с единицы, т.к. это первый кадр в видео.
- property checksum: str | None¶
Незаполнено, если не удалось прочитать файл в момент создания объекта.
- property resolution: catframes.Resolution | None¶
Незаполнено, если не удалось прочитать файл в момент создания объекта.
- class catframes.OverlayModel(warning: str, filename: str, foldername: str, symlink: bool, mtime: datetime.datetime | None, size: int | None, resolution: catframes.Resolution | None, numdir: int, numdirs: int, numvideo: int, vtime: datetime, machine: str, node: str)¶
Вся информация, которая может быть использована в оверлеях. Она для всех оверлеев кадра одинаковая, так что подготавливается один раз перед отрисовкой кадра.
- warning: str¶
Как правило это пустая строка. Задача этого поля — сообщать пользователю об очень-очень странных вещах вроде подмены кадра на диске прямо перед встраиванием в видео. Если сюда что-то записано, значит это серьёзно, но это не значит, что нужно останавливать сжатие. Напротив, об инциденте нужно рассказать всем.
Не заполняется, когда картинку не удалось открыть: вместо картинки будет показана одноцветная заглушка с названием ошибки по центру. Оверлеи будут нанесены на эту заглушку как обычно.
- mtime: datetime.datetime | None¶
Местное время последнего изменения файла на текущий момент.
Не заполняется, если файла не оказалось на диске.
- size: int | None¶
Размер файла в байтах на текущий момент.
Не заполняется, если файла не оказалось на диске.
- resolution: catframes.Resolution | None¶
Исходное разрешение кадра на текущий момент.
Не заполняется, если файла нет или его не удалось открыть.
- numdir: int¶
Номер кадра в текущей директории, начиная с единицы. Если мы пропускаем N кадров опцией
--trim-start=N
, и N меньше числа кадров в первой директории, у первого кадра видео этот номер будет равен N+1.
- catframes.OverlayTemplate¶
Скомпилированный в функцию шаблон, по которому формируется текст оверлея.
alias of
Callable
[[OverlayModel
],str
]
- class catframes.Layout¶
Кадр как таблица три-на-три. Назначаемые пользователем оверлеи располагаются в боковых ячейках, выровненные в сторону края. Отсчёт индексов ячеек ведётся от левого верхнего угла. Центр занять нельзя по очевидным причинам.
- put(xpos: int, ypos: int, overlay: OverlayTemplate)¶
Установить в ячейку шаблон оверлея.
- Исключение:
ValueError – ячейка уже занята.
Работа с размерами¶
- class catframes.ResolutionUtils¶
Модуль вспомогательных функций, связанных с разрешениями.
- static round(value: float) int ¶
Округляет вычисленный размер стороны. Поддерживаемые форматы видео могут иметь ограничения, поэтому это округление не обязательно идёт до ближайшего целого. Имеет смысл использовать как финальный этап выбора разрешения видео.
- static get_scale_size(src: Resolution, goal: Resolution) Optional[Resolution] ¶
Пропорционально меняет разрешение, чтобы исходник вписался в целевое разрешение без зазора. None означает либо рекомендацию кадрировать картинку вместо масштабирования, либо что она уже идеально вписывается.
- static get_crop_size(src: Resolution, goal: Resolution) Resolution ¶
Следует применять только если
get_scale_size()
вернул None. Если соотношение сторон отличается, размер будет меньше целевого по одной стороне.
- class catframes.ResolutionStatistics(frames: Sequence[Frame])¶
Знает, какие разрешения как часто используются.
- sort_by_count_desc() Sequence[Tuple[Resolution, int]] ¶
Возвращает разрешения в порядке убывания числа кадров.
- choose() Resolution ¶
Решает, какое разрешение лучше использовать для видео.
Совмещает самую частую ширину и самую частую высоту, которые не меньше, чем средние этих показателей. Если есть несколько значений ширины или высоты с одинаковой частотой, выбираются максимальные числа.
Если соотношение сторон непостоянно, этот метод приведёт к добавлению полей, возможно, на всех кадрах, но почти все кадры впишутся в итоговое разрешение без снижения качества.
Пример:
Ширина
Высота
Количество кадров
1280
720
3000
800
800
2000
Поскольку
средневзвешенная ширина равна 1088,
средневзвешенная высота — 752,
800 меньше 1088,
720 меньше 752,
выбрано может быть только разрешение 1280×800.
У кадров 800×800 появятся поля по бокам, у кадров 1280×720 — сверху и снизу.
Рендеринг¶
- class catframes.FrameResponse(data: bytes, content_type: str)¶
Ответ сервера на запрос кадра. Предполагается, что раз ответ есть, это HTTP OK.
- class catframes.FrameView(resolution: Resolution)¶
Абстрактное представление кадра. Отвечает как за подгонку разрешения (обязательно), а также за любую другую обработку: добавление надписей, подстраивание яркости и контрастности и т.п.
- abstract apply(frame: Frame) FrameResponse ¶
Получить оформленный кадр как набор байт. Выбрасывание исключений здесь приведёт к пятисотой ошибке в HTTP-ответе.
- class catframes.PillowFrameView(resolution: Resolution)¶
Базовые классы:
FrameView
Каркас для гарантированно однопоточного рендеринга библиотекой Pillow. Синхронизация позволяет использовать один и тот же холст многократно, не нагружая кучу и сборщик мусора.
- _draw: ImageDraw¶
2D-контекст для рисования на холсте.
- apply(frame: Frame) FrameResponse ¶
Создаёт картинку прямо в ОЗУ.
- abstract _render(frame: Frame)¶
Рисует на холсте исходный кадр и что угодно поверх него. Выбрасывание исключений приведёт к пятисотой ошибке. FFmpeg при получении такого статуса прекращает работу, так что лучше делать метод устойчивым к любым проблемам.
- _clear(color)¶
Тип аргумента допустим любой из тех, что понимает Pillow.
- _find_font(size: int) FreeTypeFont ¶
Ищет в системе что-нибудь из популярных юникодных моноширинных TrueType-шрифтов.
В документации Pillow сказано, что файлы TrueType-шрифтов могут оставаться открытыми всё время существования объекта шрифта. Вроде бы, это не страшно, ведь файл открывается на чтение, но в Windows есть лимит одновременно открытых программой файлов, поэтому, думаю, лучше использовать один объект для обработки всех кадров.
- Результат:
первый попавшийся шрифт.
- Исключение:
ValueError – ни один из ожидаемых шрифтов не найден.
- class catframes.DefaultFrameView(resolution: Resolution, margin_color: str, layout: Layout)¶
Базовые классы:
PillowFrameView
Масштабирует, добавляет поля при необходимости, накладывает текстовые индикаторы, а если файл внезапно стал недоступен, создаёт красный кадр-заглушку с названием ошибки по центру.
- Исключение:
ValueError – ошибки в параметрах или в системе не найден шрифт.
Сохранение видео¶
- class catframes.Quality(value)¶
Абстракция над бесконечными настройками качества FFmpeg.
- HIGH = (8, 4)¶
Очень высокое, но всё же с потерями. Подходит для художественных таймлапсов, где важно сохранить текстуру, световые переливы, зернистость камеры. Битрейт — как у JPEG 75.
- MEDIUM = (16, 18)¶
Подойдёт почти для любых задач. Зернистость видео пропадает, градиенты становятся чуть грубее, картинка может быть чуть мутнее, но детали легко узнаваемы.
- POOR = (22, 31)¶
Некоторые мелкие детали становятся неразличимыми.
- get_h264_crf(fps: int) int ¶
Constant Rate Factor меняет битрейт для поддержания постоянного уровня качества. Метрика качества в кодеке связана с движением — медленные объекты считаются более заметными.
При повышении частоты кадров, детали в каждом отдельном кадре становятся всё менее различимыми для зрителей, поэтому кодек при том же CRF порождает меньшие файлы.
Всё это логично для фильмов, но плохо для видеонаблюдения, где важен каждый кадр. Данный метод корректирует CRF обратно по частоте смены кадров.
- class catframes.OutputOptions(frame_rate: int, quality: Quality, destination: Path, overwrite: bool, limit_seconds: int | None)¶
Это опции сохранения видеозаписи. Грубо говоря, опции FFmpeg. Они не влияют ни на выбор разрешения, ни на обработку кадров. Ограничение длины не влияет на выбор разрешения, т.к. цель опции — заранее посмотреть, как будет выглядеть результат, поэтому и влияние на результат должно быть минимальным.
- make(frames: Sequence[Frame], view: FrameView)¶
Обрабатывает с помощью
FrameView
и соединяет кадры последовательности в видеозапись. Сортировка последовательности, как и валидация всех опций, должны быть сделаны заранее.Всё происходит в ОЗУ. Диск используется только для
чтения кадров,
хранения списка кадров в текстовом временном файле,
сохранения видео.
Скрипт ответственнен примерно за одну пятую суммарного с FFmpeg потребления памяти.