Пишем простой плагин для VirtualDub

Несмотря на то что обработка видео не спеша переезжает на OpenCL / CUDA VirtualDub остается удобным средством для простых действий с видео. Обрезка кадра, добавление фильтров или наложение выполняется гораздо удобнее чем из консоли ffmpeg. Кроме того за годы существования была разработана масса фильтров позволяющие выполнять многие операции быстро и удобно. Несмотря на простоту SDK, при написании плагина возникают некоторые нюансы. Статья посвящена работе с ними.

qouaw1my2hgzodzbmlxecg6dtzm.png



SDK доступно по ссылке с сайта автора. Последняя на данный момент версия 1.1 (VDPluginSDK-1.1.zip). Скачиваем и распаковываем в удобную для вас папку. Внутри находится файл справки PluginSDK.chm, частичным переложением которого этот текст и является. Разработка будет вестись в Microsoft Visual Studio Community 2015, можно использовать как более старые так и более новые версии. Для проверки настройки окружения можно воспользоваться файлами проектов с примерами лежащие в папке src, Samples.sln для новых версий студии или SamplesVC6.dsw для старого доброго Visual Studio 6. После сборки примеров в папке out\Release или out\Debug появится файл SampleVideoFilter.vdf. Это и есть тестовый фильтр. Для проверки достаточно положить его в папку VirtualDub\plugins и добавить из меню фильтров. Если всё работает значит Visual Studio установлен корректно.

sff0oor1mxxka64xtlgxbyoohsm.png


В качестве примера напишем фильтр с нуля. Туториал рассчитан на начинающих или вспоминающих Win32 API. Создаём в студии пустой проект динамической библиотеки DLL.

sk0-yn6z2ard0xhhlbaplfcvunw.png


Плагины для VirtualDub имеют расширение vdf, поэтому чтобы не переименовывать его каждый раз меняем расширение в свойства проекта Properties→General→Target extension на .vdf. Меняем для всех конфигураций, поэтому не забываем переключить их на вкладке настроек Configuration: на All Configurations и Platform на All platforms.

czb-aiiveifaniki5a0t1z-1yf4.png


Копируем в проект папку include из распакованного SDK и добавляем файлы из него в проект через Atl-Shift-A или меню Add→Existing Item. Для работы нам понадобятся файлы заголовков из папки include и набор файлов хелпера VDXFrame. Не забываем добавить папку include в список папок где система будет их искать. Делается это из Properties→VC++ Directories→Include Directories, добавляем ссылку на корень проекта в виде $(ProjectDir)\include.

prdsfcurikgcammpur-racthciu.png


Добавляем в проект библиотеку VDXFrame, в примерах она используется в виде отдельного модуля, но так как лицензия позволяет, добавим её в виде исходного кода. Создадим в каталоге проекта папку src и скопируем в неё из SDK файлы VideoFilter.cpp, VideoFilterEntry.cpp, VideoFilterDialog.cpp и stdafx.cpp. Далее скопируем файл заголовка из include\stdafx.h в ранее созданную папку include. Не забываем добавить скопированные файлы в проект через Atl-Shift-A или из меню Add→Existing Item. На этом интеграция библиотеки хелпера заканчивается.

Переходим к написанию кода. Добавляем в проект новый файл main.cpp через Add→Existing Item или комбинацию клавиш Ctrl-Shift-A. Добавляем в main следующие строки

#include 
#include 

VDXFilterDefinition filterDef_blackWhite = VDXVideoFilterDefinition("Shadwork", "Black White filter", "Example for VirtualDub Plugin SDK: Applies a Black White filter to video.");

VDX_DECLARE_VIDEOFILTERS_BEGIN()
	VDX_DECLARE_VIDEOFILTER(filterDef_blackWhite)
VDX_DECLARE_VIDEOFILTERS_END()

VDX_DECLARE_VFMODULE()


Плагин может содержать в себе произвольное количество фильтров описываемых макросом VDX_DECLARE_VIDEOFILTER с параметром в виде класса VDXFilterDefinition служащим оболочкой над классом фильтра. Сам фильтр описывается тремя текстовыми полями: Автор, Название и Описание. Создадим класс фильтра с именем BlackWhiteFilter, у автора VirtualDub классы именуются с использованием CamelCase поэтому создаем новый класс унаследованный от VDXVideoFilter в файле BlackWhiteFilter.h. Переменная g_VFVAPIVersion будет содержать версию API. Функции определенные с virtual являются частью SDK, а метод ToBlackAndWhite будет реализовывать преобразование картинки.

#include 
#include 

#ifndef FILTER_VD_BLACK_WHITE
#define FILTER_VD_BLACK_WHITE

extern int g_VFVAPIVersion;

class BlackWhiteFilter : public VDXVideoFilter {
public:
	virtual uint32 GetParams();
	virtual void Start();
	virtual void Run();

protected:
	void ToBlackAndWhite(void *dst, ptrdiff_t dstpitch, const void *src, ptrdiff_t srcpitch, uint32 w, uint32 h);
};

#endif 


Реализацию пишем в файле BlackWhiteFilter.cpp, метод Start () выполняется первым, он предназначен для любых предварительных действий, например для определения совместимости с набором инструкций AVX или поддержки CUDA. Оставляем его пока пустым. Хелпер VDXFrame обеспечивает в пределах видимости этого класса указатель на экземпляр класса VDXFilterActivation с именем fa, содержащий информацию о кадре и буферах.
Метод GetParams () используется VirtualDub для определения совместимости фильтра, он должен вернуть битовую маску из перечисления FILTERPARAM

  • FILTERPARAM_SWAP_BUFFERS создаётся два независимых буфера для входного и выходного кадров, рекомендуется использовать всегда чтобы не создавать такие буфера руками
  • FILTERPARAM_NEEDS_LAST передаёт в фильтр не только текущий кадр, но и идущий перед ним, используется для фильтров состояние которых зависит от предыдущего кадра
  • FILTERPARAM_SUPPORTS_ALTFORMATS информирует VirtualDub что плагин поддерживает кодирование кадра отличное от RGB32, например YUV, что позволяет оптимизировать вычисления
  • FILTERPARAM_ALIGN_SCANLINES фильтр требует выравнивания данных на 16 байт, а значит не поддерживает например длину строки 13 байт
  • FILTERPARAM_PURE_TRANSFORM поведение фильтра зависит только от данных в буфере кадра, позволяет ускорить обработку и отображение фильтра
  • FILTERPARAM_NOT_SUPPORTED фильтр не поддерживает входные данные в данном формате и работать не будет

Для фильтра который будет конвертировать изображение RGB32 в черно-белое нам подойдет FILTERPARAM_SWAP_BUFFERS и FILTERPARAM_PURE_TRANSFORM. Если мы хотим поддерживать кодировку цвета отличную от RGB32 и версию SDK меньше 12 пишем проверку на g_VFVAPIVersion и если она поддержана проверяем формат полученного изображения в поле fa→src.mpPixmapLayout→format. Ранние версии VirtualDub не поддерживали представление цвета отличное от RGB32. Для упрощения обработки писать будем придерживаясь формата RGB32, но вообще VirtualDub поддерживает большой список форматов, перечисленный в VDXPixmapFormat.

uint32 BlackWhiteFilter::GetParams() {
	if (g_VFVAPIVersion >= 12) {
		switch (fa->src.mpPixmapLayout->format) {
		case nsVDXPixmap::kPixFormat_XRGB8888:
			break;
		default:
			return FILTERPARAM_NOT_SUPPORTED;
		}
	}

	fa->dst.offset = 0;
	return FILTERPARAM_SWAP_BUFFERS;
}


Обработка кадра выполняется методом Run (). Данные о кадре и входном и выходном буферах хранятся в переменной fa являющаяся экземпляром класса VDXFilterActivation. VirtualDub поддерживает обрезку кадра, поэтому алгоритм обработки можно оптимизировать получив информацию о выбранном пользователем окне с координатами x1, y1, x2, y2. Данные кадра хранятся в объектах src и dst, соответственно входной и выходной буфер.

class VDXFilterActivation {
public:
	const VDXFilterDefinition *filter;		// 
	void *filter_data;
	VDXFBitmap&	dst;
	VDXFBitmap&	src;
	VDXFBitmap	*_reserved0;
	VDXFBitmap	*const last;
	uint32		x1;
	uint32		y1;
	uint32		x2;
	uint32		y2;

	VDXFilterStateInfo	*pfsi;
	IVDXFilterPreview	*ifp;
	IVDXFilterPreview2	*ifp2;			// (V11+)

	uint32		mSourceFrameCount;		// (V14+)
	VDXFBitmap *const *mpSourceFrames;	// (V14+)
	VDXFBitmap *const *mpOutputFrames;	// (V14+)
};


Если мы продолжаем писать код с поддержкой SDK меньше 12 версии то реализация метода Run () примет такой вид

void BlackWhiteFilter::Run() {
	if (g_VFVAPIVersion >= 12) {
		const VDXPixmap& pxdst = *fa->dst.mpPixmap;
		const VDXPixmap& pxsrc = *fa->src.mpPixmap;

		switch (pxdst.format) {
		case nsVDXPixmap::kPixFormat_XRGB8888:
			ToBlackAndWhite(pxdst.data, pxdst.pitch, pxsrc.data, pxsrc.pitch, pxsrc.w, pxsrc.h);
			break;
		}
	}
	else {
		ToBlackAndWhite(fa->dst.data, fa->dst.pitch, fa->src.data, fa->src.pitch, fa->dst.w, fa->dst.h);
	}
}


От версии которую поддерживает плагин зависит место хранения сырых данных в структуре. Итак, в функцию ToBlackAndWhite будет передано 6 параметров

  1. void *dst0 — выходной буфер кадра
  2. ptrdiff_t dstpitch — полная длина строки в байтах выходного буфера
  3. const void *src0 — входной буфер кадра
  4. ptrdiff_t srcpitch — полная длина строки входного буфера
  5. uint32 w — ширина кадра в пикселях
  6. uint32 h — высота кадра в пикселях

Для упрощения кода мы проигнорируем параметры обрезки, поэтому кадр будет обрабатываться с одинаковой скоростью вне зависимости от параметра Crop в настройках. Точка в буфере хранится в формате kPixFormat_XRGB8888 и занимает 32 бита. Реализуем простейшее преобразование кадра в черно-белый. Задача оптимизации у нас не стоит, поэтому считать будем по формуле с расчетом в арифметике с плавающей запятой

GRAY = 0.299 * R + 0.587 * G + 0.114 * B

Организуем два цикла, один проходит по строкам, а второй по точкам, граничный уровень для определения цвета точки примем равным 128.

void BlackWhiteFilter::ToBlackAndWhite(void *dst0, ptrdiff_t dstpitch, const void *src0, ptrdiff_t srcpitch, uint32 w, uint32 h) {
	char *dst = (char *)dst0;
	const char *src = (const char *)src0;
	for (uint32 y = 0; y> 8) + 0.114f *((data & 0x00ff0000) >> 16);
			dstline[x] = gray < 128 ? 0x00000000 : 0x00ffffff;
		}
		src += srcpitch;
		dst += dstpitch;
	}
}


Собираем плагин, копируем файл Windows-VirtualDub-Plugin-BlackWhite.vdf в папку plugins VirtualDub и делаем его активным. В списке он будет виден под названием, которое мы задали в классе VDXFilterDefinition — Black White filter. Плагин собранный для 64 битной версии не будет видно в 32 битной версии VirtualDub, поэтому не забываем проверить активную конфигурацию проекта.

qouaw1my2hgzodzbmlxecg6dtzm.png


Плагин без настроек довольно уныл, добавим возможность настройки и кнопку предварительного просмотра. Для этого нам бы следовало погрузиться в дебри Win32 API, но по этой теме написано достаточно книг, поэтому не будем вдаваться в детали.
Для визуального представления окна настройки нам понадобится диалоговое окно. Создаем новый файл ресурсов через меню Ctrl-Shift-A → Resource → Resource File с именем Resource.rc. Добавим в него диалоговое окно через меню Add Resource → Dialog и изменим ему имя на IDD_DIALOG_BLACKWHITE_SETTING. По умолчанию у нас уже есть две кнопки Ok и Cancel. Создавать ресурсы лучше в английской локали, иначе можно получить проблему с не читаемым русским шрифтом на кнопке Отмена. Добавим на экран кнопку Preview с именем IDC_SLIDER_THRESHOLD. Чтобы потом не возвращаться добавим остальные элементы управления для настроек, это будет слайдер для изменения порогового значения IDC_SLIDER_THRESHOLD и checkbox IDC_CHECK_INVERTED позволяющий инвертировать картинку. Сверстать это можно например так.

jv2ioogd-rwqqjpg8e0xthabnw4.png


Создадим класс диалога BlackWhiteFilterDialog унаследованный от VDXVideoFilterDialog.

#include 
#include 
#include 
#include 
#include 

#ifndef FILTER_VD_BLACK_WHITE_DIALOG
#define FILTER_VD_BLACK_WHITE_DIALOG

class BlackWhiteFilterDialog : public VDXVideoFilterDialog {
public:
	BlackWhiteFilterDialog(IVDXFilterPreview *ifp);
	bool Show(HWND parent);
	virtual INT_PTR DlgProc(UINT msg, WPARAM wParam, LPARAM lParam);
protected:
	IVDXFilterPreview *const mifp;

	bool OnInit();
	bool OnCommand(int cmd);
	void OnDestroy();
};

#endif 


В конструктор передаётся ссылка на класс IVDXFilterPreview который управляет окном предварительного просмотра, локальную ссылку мы будем хранить в переменной mifp.

BlackWhiteFilterDialog::BlackWhiteFilterDialog(IVDXFilterPreview *ifp):mifp(ifp){
}


Метод Show (HWND parent) перегружен вызовом конструктора родителя и использует в качестве параметра идентификатор ресурса диалога настроек IDD_DIALOG_BLACKWHITE_SETTING

bool BlackWhiteFilterDialog::Show(HWND parent) {
	return 0 != VDXVideoFilterDialog::Show(NULL, MAKEINTRESOURCE(IDD_DIALOG_BLACKWHITE_SETTING), parent);
};


DlgProc используется для обработки сообщений от диалогового окна и реализует обработку жизненного цикла диалога в методах OnInit (), OnDestroy () и обработку событий от элементов управления в OnCommand.

INT_PTR BlackWhiteFilterDialog::DlgProc(UINT msg, WPARAM wParam, LPARAM lParam) {
	switch (msg) {
	case WM_INITDIALOG:
		return !OnInit();

	case WM_DESTROY:
		OnDestroy();
		break;

	case WM_COMMAND:
		if (OnCommand(LOWORD(wParam)))
			return TRUE;
		break;

	case WM_HSCROLL:
		if (mifp)
			mifp->RedoFrame();
		return TRUE;
	}

	return FALSE;
}


Для начала обработаем закрытие диалога по кнопкам Ok и Cancel. Кроме того нам понадобится обработчик Preview, управляющий отображением окна предварительного просмотра через метод Toggle ((VDXHWND)mhdlg).

bool BlackWhiteFilterDialog::OnCommand(int cmd) {
	switch (cmd) {
		case IDOK:
			EndDialog(mhdlg, true);
			return true;

		case IDCANCEL:
			EndDialog(mhdlg, false);
			return true;
		case IDC_PREVIEW:
			if (mifp)
				mifp->Toggle((VDXHWND)mhdlg);
			return true;
	}
	return false;
}


Класс для работы с диалогом написан, теперь его необходимо вызвать, для этого перегружаем в классе BlackWhiteFilter метод Configure (VDXHWND hwnd) и реализуем его

bool BlackWhiteFilter::Configure(VDXHWND hwnd) {
	BlackWhiteFilterDialog dlg(fa->ifp);
	return dlg.Show((HWND)hwnd);
}


Собираем проект, копируем файл плагина в папку VirtualDub, добавляем новый фильтр в список и видим наш диалог и доступную кнопку Preview.

ary2heefy0jjgaaxdqfv_9v7_jc.png


Окно конфигурации у нас есть, но настроек у фильтра пока нет, приступаем к реализации. Настройки будем хранить в классе BlackWhiteFilterConfig содержащем всего две переменные, mTreshold как величину порогового значения и флаг инверсии mInvert.

#ifndef FILTER_VD_BLACK_WHITE_CONFIG
#define FILTER_VD_BLACK_WHITE_CONFIG

class BlackWhiteFilterConfig {
public:
	BlackWhiteFilterConfig()
	{
		mTreshold = 128;
		mInvert = 0;
	}

public:
	int mTreshold;
	int mInvert;
};

#endif 


Отредактируем класс BlackWhiteFilterDialog, добавив в него два экземпляра класса BlackWhiteFilterConfig для хранения конфигурации mConfigNew и mConfigOld. Эти переменные будут хранить старое и измененное состояние настроек и понадобятся нам для работы кнопки
Ok и Cancel. Отредактируем конструктор, добавив в него параметр хранящий настройки и инициализацию конфигурации.

BlackWhiteFilterDialog::BlackWhiteFilterDialog(BlackWhiteFilterConfig& config, IVDXFilterPreview *ifp):mifp(ifp){
	mConfigNew = config;
}


Настройки должны где-то храниться, добавляем в класс BlackWhiteFilter переменную BlackWhiteFilterConfig mConfig и меняем инициализацию класса BlackWhiteFilterDialog в методе Configure на новую.

bool BlackWhiteFilter::Configure(VDXHWND hwnd) {
	BlackWhiteFilterDialog dlg(mConfig, fa->ifp);
	return dlg.Show((HWND)hwnd);
}


Теперь необходимо снова поработать с элементами управления Win32. В классе BlackWhiteFilterDialog напишем два метода связывающих нашу конфигурацию и ее реализацию в диалоге.

void BlackWhiteFilterDialog::LoadFromConfig() {
	SendDlgItemMessage(mhdlg, IDC_SLIDER_THRESHOLD, TBM_SETPOS, TRUE, mConfigNew.mTreshold);
	SendMessage(mhdlg, IDC_CHECK_INVERTED, mConfigNew.mInvert, 0);
}

bool BlackWhiteFilterDialog::SaveToConfig() {
	int threshold = SendDlgItemMessage(mhdlg, IDC_SLIDER_THRESHOLD, TBM_GETPOS, 0, 0);
	int inverted = SendDlgItemMessage(mhdlg, IDC_CHECK_INVERTED, BM_GETCHECK, 0, 0);

	if (threshold != mConfigNew.mTreshold || inverted!= mConfigNew.mInvert)
	{
		mConfigNew.mTreshold = threshold;
		mConfigNew.mInvert = inverted;
		return true;
	}
	return false;
}


Осталось использовать эти два метода в жизненном цикле диалога. В OnCommand для кнопки Ok вызываем SaveToConfig (), а для кнопки Cancel восстанавливаем старый набор настроек присваиванием mConfigNew = mConfigOld. Начальные параметры диалога настраиваются в методе OnInit (), диапазон слайдера устанавливается в 0–255 и на него устанавливается фокус.

bool BlackWhiteFilterDialog::OnInit() {
	mConfigOld = mConfigNew;
	// Set up slider to range 0-255
	SendDlgItemMessage(mhdlg, IDC_SLIDER_THRESHOLD, TBM_SETRANGE, TRUE, MAKELONG(0, 255));
	LoadFromConfig();
	// gain focus to slide control
	HWND hwndFirst = GetDlgItem(mhdlg, IDC_SLIDER_THRESHOLD);
	if (hwndFirst)
		SendMessage(mhdlg, WM_NEXTDLGCTL, (WPARAM)hwndFirst, TRUE);
	// init preview button
	HWND hwndPreview = GetDlgItem(mhdlg, IDC_PREVIEW);
	if (hwndPreview && mifp) {
		EnableWindow(hwndPreview, TRUE);
		mifp->InitButton((VDXHWND)hwndPreview);
	}
	return false;
}


Изменение настроек необходимо отобразить в окне предварительного просмотра с помощью метода RedoFrame (), для этого отредактируем метод DlgProc добавив вызов сохранения параметров в методе в обработчике WM_HSCROLL для слайдера с проверкой что окно Preview включено if (mifp && SaveToConfig ())mifp→RedoFrame (). Для обработки CheckBox допишем в метод OnCommand условие для case на идентификатор IDC_CHECK_INVERTED и выполним такое же обновление.

case IDC_CHECK_INVERTED:
	if (mifp && SaveToConfig())mifp->RedoFrame();
	return true;


Перепишем метод ToBlackAndWhite для использования конфигурации, учитывая два параметра, инверсию и пороговое значения. Константа BST_UNCHECKED унаследована от Win32 API и используется как значение флага true/false.

if (mConfig.mInvert == BST_UNCHECKED) {
	dstline[x] = gray < mConfig.mTreshold ? 0x00000000 : 0x00ffffff;
}
else {
	dstline[x] = gray > =mConfig.mTreshold ? 0x00000000 : 0x00ffffff;
}


Собираем проект и опять тестируем фильтр в VirtualDub, включение инверсии превратила милого котика в нечто готические страшное.

cv8be4nddekgjia619ts2maxvoe.png


Нам осталось совсем чуть-чуть до финала. Фильтры VirtualDub поддерживают сохранение параметров в файл настроек, для этого нужно сериализировать наш класс настроек. Для этого существует макрос VDXVF_DECLARE_SCRIPT_METHODS () который добавляется в заголовок класса BlackWhiteFilter и набор методов для реализации записи и отображения настроек GetSettingString, GetScriptString и метод ScriptConfig для синтаксического разбора параметров из файла настроек. Количество и там аргументов задаются в макросе VDXVF_DEFINE_SCRIPT_METHOD в виде последнего параметра. Новая версия класса BlackWhiteFilter будет выглядеть так

#include 
#include 
#include 

#ifndef FILTER_VD_BLACK_WHITE
#define FILTER_VD_BLACK_WHITE

extern int g_VFVAPIVersion;

class BlackWhiteFilter : public VDXVideoFilter {
	public:
		virtual uint32 GetParams();
		virtual void Start();
		virtual void Run();
		virtual bool Configure(VDXHWND hwnd);
		virtual void GetSettingString(char *buf, int maxlen);
		virtual void GetScriptString(char *buf, int maxlen);
		VDXVF_DECLARE_SCRIPT_METHODS();
	protected:
		void ToBlackAndWhite(void *dst, ptrdiff_t dstpitch, const void *src, ptrdiff_t srcpitch, uint32 w, uint32 h);
		BlackWhiteFilterConfig mConfig;
		void ScriptConfig(IVDXScriptInterpreter *isi, const VDXScriptValue *argv, int argc);
};

#endif 


Реализуем методы которых не хватает. Декларируем количество параметров и их тип в макросе VDXVF_DEFINE_SCRIPT_METHOD, у нас их два, оба целочисленные, поэтому строка инициализации будет «ii». Список поддерживаемых форматов можно посмотреть в классе IVDXScriptInterpreter, доступны целые, дробные и строковые параметры. Метод GetSettingString отображает параметры в строке настроек, он нужен для человека который сможет быстро посмотреть параметры в окне Filters, в колонке описания Filter. Метод GetScriptString форматирует параметры для сохранения их в файл VirtualDub configuration (*.vcf) и последующего их чтения методом ScriptConfig.

VDXVF_BEGIN_SCRIPT_METHODS(BlackWhiteFilter)
VDXVF_DEFINE_SCRIPT_METHOD(BlackWhiteFilter, ScriptConfig, "ii")
VDXVF_END_SCRIPT_METHODS()

void BlackWhiteFilter::GetSettingString(char *buf, int maxlen) {
	SafePrintf(buf, maxlen, " (Treshold:%d, Invert:%d)", mConfig.mTreshold, mConfig.mInvert);
}

void BlackWhiteFilter::GetScriptString(char *buf, int maxlen) {
	SafePrintf(buf, maxlen, "Config(%d, %d)", mConfig.mTreshold, mConfig.mInvert);
}

void BlackWhiteFilter::ScriptConfig(IVDXScriptInterpreter *isi, const VDXScriptValue *argv, int argc) {
	mConfig.mTreshold = argv[0].asInt();
	mConfig.mInvert = argv[1].asInt();
}


Добавив данный код и собрав плагин мы получим возможность видеть настройки фильтра в окне Filters и сохранять их в файл через меню файл Save processing setting.

vs1kf4cbgcbu-3vrnwnfeanbxz4.png


По умолчанию проект собирается с зависимостями от установленной в системе VC Runtime, если планируется его использование на других компьютерах, при сборке необходимо указать параметр Multi-threaded (/MT) из меню настроек Configuration→C/C++→Code Generation→Runtime Library. Плагин увеличит свой размер в десять раз, но пользователям не придется подбирать Runtime под версию Visual Studio которую использовал разработчик.

0pvskbg6m2awuuufte2qp94btwu.png


Код проекта доступен на github. Материал нацелен на людей которым нужно сделать что-то быстро, а вспоминать тонкости работы с Win32 API неохота. Мне этот плагин понадобился для переноса видео на платформу с однобитным представлением цвета, а прогонять каждый раз набор кадров через XnView надоело.

© Habrahabr.ru