Несмотря на то что обработка видео не спеша переезжает на OpenCL / CUDA VirtualDub остается удобным средством для простых действий с видео. Обрезка кадра, добавление фильтров или наложение выполняется гораздо удобнее чем из консоли ffmpeg. Кроме того за годы существования была разработана масса фильтров позволяющие выполнять многие операции быстро и удобно. Несмотря на простоту SDK, при написании плагина возникают некоторые нюансы. Статья посвящена работе с ними.
SDK доступно по ссылке с сайта автора. Последняя на данный момент версия 1.1 (VDPluginSDK-1.1.zip). Скачиваем и распаковываем в удобную для вас папку. Внутри находится файл справки PluginSDK.chm, частичным переложением которого этот текст и является. Разработка будет вестись в Microsoft Visual Studio Community 2015, можно использовать как более старые так и более новые версии. Для проверки настройки окружения можно воспользоваться файлами проектов с примерами лежащие в папке src, Samples.sln для новых версий студии или SamplesVC6.dsw для старого доброго Visual Studio 6. После сборки примеров в папке outRelease или outDebug появится файл SampleVideoFilter.vdf. Это и есть тестовый фильтр. Для проверки достаточно положить его в папку VirtualDubplugins и добавить из меню фильтров. Если всё работает значит Visual Studio установлен корректно.
В качестве примера напишем фильтр с нуля. Туториал рассчитан на начинающих или вспоминающих Win32 API. Создаём в студии пустой проект динамической библиотеки DLL.
Плагины для VirtualDub имеют расширение vdf, поэтому чтобы не переименовывать его каждый раз меняем расширение в свойства проекта Properties→General→Target extension на .vdf. Меняем для всех конфигураций, поэтому не забываем переключить их на вкладке настроек Configuration: на All Configurations и Platform на All platforms.
Копируем в проект папку include из распакованного SDK и добавляем файлы из него в проект через Atl-Shift-A или меню Add→Existing Item. Для работы нам понадобятся файлы заголовков из папки include и набор файлов хелпера VDXFrame. Не забываем добавить папку include в список папок где система будет их искать. Делается это из Properties→VC++ Directories→Include Directories, добавляем ссылку на корень проекта в виде $(ProjectDir)include.
Добавляем в проект библиотеку VDXFrame, в примерах она используется в виде отдельного модуля, но так как лицензия позволяет, добавим её в виде исходного кода. Создадим в каталоге проекта папку src и скопируем в неё из SDK файлы VideoFilter.cpp,VideoFilterEntry.cpp,VideoFilterDialog.cpp и stdafx.cpp. Далее скопируем файл заголовка из includestdafx.h в ранее созданную папку include. Не забываем добавить скопированные файлы в проект через Atl-Shift-A или из меню Add→Existing Item. На этом интеграция библиотеки хелпера заканчивается.
Переходим к написанию кода. Добавляем в проект новый файл main.cpp через Add→Existing Item или комбинацию клавиш Ctrl-Shift-A. Добавляем в main следующие строки
#include <vd2/VDXFrame/VideoFilter.h>
#include <BlackWhiteFilter.h>
VDXFilterDefinition filterDef_blackWhite = VDXVideoFilterDefinition<BlackWhiteFilter>("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 <vd2/VDXFrame/VideoFilter.h>
#include <vd2/VDXFrame/VideoFilterEntry.h>
#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 параметров
- void *dst0 – выходной буфер кадра
- ptrdiff_t dstpitch — полная длина строки в байтах выходного буфера
- const void *src0 — входной буфер кадра
- ptrdiff_t srcpitch — полная длина строки входного буфера
- uint32 w — ширина кадра в пикселях
- 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<h; ++y) {
// Get scanline
uint32 *srcline = (uint32 *)src;
uint32 *dstline = (uint32 *)dst;
for (uint32 x = 0; x<w; ++x) {
// Process pixels
uint32 data = srcline[x];
float gray = 0.299f * (data & 0x000000ff) + 0.587f * ((data & 0x0000ff00) >> 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, поэтому не забываем проверить активную конфигурацию проекта.
Плагин без настроек довольно уныл, добавим возможность настройки и кнопку предварительного просмотра. Для этого нам бы следовало погрузиться в дебри 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 позволяющий инвертировать картинку. Сверстать это можно например так.
Создадим класс диалога BlackWhiteFilterDialog унаследованный от VDXVideoFilterDialog.
#include <windows.h>
#include <commctrl.h>
#include <resource.h>
#include <vd2/VDXFrame/VideoFilterDialog.h>
#include <vd2/VDXFrame/VideoFilter.h>
#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.
Окно конфигурации у нас есть, но настроек у фильтра пока нет, приступаем к реализации. Настройки будем хранить в классе 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, включение инверсии превратила милого котика в нечто готические страшное.
Нам осталось совсем чуть-чуть до финала. Фильтры VirtualDub поддерживают сохранение параметров в файл настроек, для этого нужно сериализировать наш класс настроек. Для этого существует макрос VDXVF_DECLARE_SCRIPT_METHODS() который добавляется в заголовок класса BlackWhiteFilter и набор методов для реализации записи и отображения настроек GetSettingString, GetScriptString и метод ScriptConfig для синтаксического разбора параметров из файла настроек. Количество и там аргументов задаются в макросе VDXVF_DEFINE_SCRIPT_METHOD в виде последнего параметра. Новая версия класса BlackWhiteFilter будет выглядеть так
#include <vd2/VDXFrame/VideoFilter.h>
#include <vd2/VDXFrame/VideoFilterEntry.h>
#include <BlackWhiteFilterDialog.h>
#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.
По умолчанию проект собирается с зависимостями от установленной в системе VC Runtime, если планируется его использование на других компьютерах, при сборке необходимо указать параметр Multi-threaded (/MT) из меню настроек Configuration->C/C++->Code Generation->Runtime Library. Плагин увеличит свой размер в десять раз но пользователям не придется подбирать Runtime под версию Visual Studio которую использовал разработчик.
Код проекта доступен на github. Материал нацелен на людей которым нужно сделать что-то быстро а вспоминать тонкости работы с Win32 API неохота. Мне этот плагин понадобился для переноса видео на платформу с однобитным представлением цвета, а прогонять каждый раз набор кадров через XnView надоело.
Автор: shadwork