Delphi+OpenCV

3d109832eae7bb521949e160c1d5486f

Сначала была мечта…

Что-то похожее на робототехническую систему, с двумя подвижными камерами, способностью отслеживать (направлять «взгляд» на) заданный объект и определять расстояние до объекта. И это был 2012 год. Но так как я больше программист нежели железячник, то все началось с реализации существующих в то время алгоритмов. Скоро пришло осознание, что алгоритмы и их реализация не есть цель. Цель — робототехническая система. Поэтому было принято решение воспользоваться существующими библиотеками обработки изображений. Но, к сожалению, на Object Pascal готовые библиотеки алгоритмов, которые были найдены в то время, не позволяли решать поставленные задачи.

Очень понравилась OpenCV, тогда еще версия 2.4.2, но естественно без поддержки Object Pascal. Попытки других авторов трансляции заголовочных файлов OpenCV были скромными, просто как пример, концепция. Помню очень тогда взбесило, хотите Java — вот пожалуйста порт, инструменты для создания интерфейсных классов, незаметный для программиста проброс обращений в OpenCV и, самое главное, практически один в один с С++ вид программы на Java. Почти тоже самое с Python и некоторыми другими языками. Короче говоря, Дельфизм (аналог феминизма у нормальных людей) взял верх.

Так как это была еще версия OpenCV 2.4.2, с С (cdecl) x32 функциями, то работа по трансляции заголовочных файлов оказалась достаточно простой… большой, но простой, очень объемной, но простой… рутинной, но простой… Просто добавь «cdecl; external core_lib;» и переставь местами имя параметра и его тип. Даже была попытка создать свой автоматизированный инструмент для трансляции. Доступные в то время готовые инструменты выдавали дичь, которую потом править было дольше, чем переводить вручную. Ну и тем более заголовочные файлы OpenCV это не «Hello world» — очень много приходилось принимать решений о том, как правильно перевести задумки авторов в код на Object Pascal. Например, в силу различий способов возврата Var параметров функцию cvGetSize пришлось реализовывать в виде ассемблерной вставки, которая меняет местами регистры. Некоторые вещи не доработаны, например, конвертация cvImage в TBitmap (функция cvImage2Bitmap) — реализованы не все возможные форматы изображений, но это не критично. В какой-то момент нас стало двое. После публикации статьи на Хабре к проекту подключился Михаил Григорьев (@Sleuthhound). В целом, весь объем API OpenCV вплоть до версии 2.4.13 был переведен. Потом включился «Форест Гамп» — добежали до этого места, а сможем ли побежать дальше. Появились примеры с OpenCV+OpenGL, OpenCV+SDL, OpenCV+FFMPEG. Для FFMPEG x32 даже была сделана собственная трансляция заголовочных файлов. Были найдены и переведены все существующие в интернете в то время примеры программ на С, использующих OpenCV. Добавлена возможность использовать все это в FPC. Разработаны компоненты для Delphi, которые похоже «не только лишь каждый» может установить. Автоматический инсталлятор компонент, не доведенный до релиза … А все почему?

Да потому что вышел OpenCV 3.0. Ну зачем вы так… Классы, неявные преобразования… Да и вообще — x64. Object Pascal в то время так не мог. Дельфизм перешел в стадию радикального Дельфизма.

Было предпринято множество попыток транслировать С++ классы на Object Pascal. Отправным пунктом послужила статья Using C++ objects in Delphi. В основе лежат две идеи основанные на разработки прокси dll. В первом случае вызовы Object Pascal просто транслировались в вызовы функций класса OpenCV, во втором — в прокси dll разрабатывались COM интерфейсы, ссылка на которые возвращалась в вызывающую программу. Остатки этих попыток можно наблюдать в каталоге source3. Оба подхода сопоставимы по получаемому результату.

Основное, что изначально не понравилось в таких подходах:

  1. Требовалось «обернуть» все классы, что очень много и, в данном случае, очень сложно.

  2. Напрочь терялась вся гибкость самой библиотеки OpenCV по неявному преобразованию типов и еще по многим другим параметрам.

Были написаны несколько инструментов для автоматической трансляции OpenCV классов как для первого, так и для второго варианта. В одном из вариантов, для парсинга использовался llvm с собственной разработкой перевода заголовков llvm на Object Pascal. Была реализована попытка написания в Object Pascal аналога JNI из Java. В конечном итоге, из-за недостатка времени и необходимости периодически кушать и спать, попытка изобрести С++ в Object Pascal тогда не удалась…

Ну да ладно, как говорят андроиды (не ОС) «Кто старое помянет, у того камера сломается…».

Что хотелось от OpenCV на Object Pascal — это максимально гибкое использование возможностей библиотеки OpenCV, с явным и неявным преобразованием типов, скрытое обращение к самой OpenCV, использование стандартных конструкций Object Pascal и их скрытое от программиста преобразование в формат C++. Часть возможностей Object Pascal для реализации этих идей существовало еще в Delphi 2005 (если не ошибаюсь): class operator, Implicit, Explicit. Но этого было мало. В любом случае требовалось для класса в Object Pascal вызывать конструктор и деструктор, а для записи (record), например, Init и Done. Для интерфейсов — все-таки их надо было как-то создавать. Т.е. такого механизма как в C++, автоматического создания и уничтожения собственных структур пока не было.

И вот свершилось — Custom Managed Records! Да еще с возможностью контроля присвоения — class operator Assign. Автоматическое создание с вызовом инициализатора и уничтожение структуры с вызовом «уничтожатора».

Второе, что сдерживало работу — это разный способ передачи в x32 параметров в функции для Object Pascal и в классах С++. Приходилось исследовать каждую функцию (речь о классах еще в OpenCV 2.4.х). Проблема решилась с переходом OpenCV 3.0 на x64, с единым соглашением о вызовах в Object Pascal и С++ (но есть нюанс).

Третье и может самое быть главное — обращение из Object Pascal к функциям класса из OpenCV. Как это выяснилось (достаточно давно) функции класса С++ можно импортировать по «декорированному» имени, а правильно их вызвать это уже дело техники. Кстати, дельфисты-китайцы сейчас очень часто этот способ используют.

В совокупности, эти три фактора уже привели к появлению попыток использования OpenCV 4.5.2 в Delphi (например, DOpenCV), правда опять же как концепция, как пример.

Так что, проснувшись одним утром, я явно услышал в щебете птиц, звуке закипающего чайника — «Пора!». Да и тут еще эти неожиданные нерабочие не выходные.

Теперь по сути вопроса.

1. Получить из DLL все функции не проблема, например, в opencv_world454.dll их 7320 штук (в том числе 6930 функций и операторов классов и 390 функций). Раздекорировать — тоже не проблема. Понять, что это за функция класса: конструктор, деструктор, оператор — тоже не проблема, смотрим документацию, ну или разбираемся сами по раздекорированному имени (это сложнее).

Например, конструктор cv::Mat(int rows, int cols, int type); имеет вид

??0Mat@cv@@QEAA@HHH@Z
public: __cdecl cv::Mat::Mat(int,int,int) __ptr64

2. Для импортированной функции пишем аналог на Object Pascal

Например, для того же конструктора

procedure Constructor_Mat(Obj: pMat; a: int; b: int; c: int);
external opencv_world_dll name '??0Mat@cv@@QEAA@HHH@Z';

Обратите внимание, что первым параметром передается указатель на сам класс (регистр RAX), что является стандартным подходом во многих языках программирования. Ну если честно, то это указатель на память с данными класса.

Передача параметров осуществляется согласно Microsoft x64 calling convention, ну почти. Одним из исключений является передача по значению не простых типов. Delphi, все не простые типы (например, записи) даже если они размером 1 байт передает как адрес, в тоже время передача по значению в классах С++ для данных размером меньше или равными 8 байт передаются в 64-битном регистре своим значением. Такое поведение наблюдается, например, для TRect (Object Pascal) и Rect=Rect2i=Rect_(C++). Вообще, несмотря на соглашения о вызовах определяющих порядок использования регистров, в способе передачи данных из Object Pascal в функцию класса С++ есть несколько отличий, но, наверное, это тема отдельного разбирательства.

В некоторых случаях импортировать по имени не получается (надо разбираться, оставим это в TODO) и приходится импортировать по индексу, что плохо. При переходе на следующую версию будут проблемы.

3.Когда-то, в умной книжке по C++, было сказано, что в C++ классы — это структуры (struct), подвергшиеся сильной радиационной и химической мутации. А действительно, что выдумывать что-то новое? Память под данные класса С++ выделяем на стадии компиляции, в месте его объявления вызываем конструктор по умолчанию (это если класс объявлен без параметров), когда не нужен — вызываем деструктор. Красота!

И вот здесь и пригодились Custom Managed Records. Их поведение ну очень уж похоже на классы в C++.

TMat = record // 96 bytes, v4.5.4
  public
    // default constructor
// Mat();
    class operator Initialize(out Dest: TMat); 
    class function Mat(): TMat; overload; static;
    class function Mat(rows, cols, &type: Int):TMat; overload; static;
// Mat(Size size, int type);
    class function Mat(const size: TSize; &type: Int): TMat; overload; static; 
// Mat(const Mat& m, const Rect& roi);
    class function Mat(const m: TMat; const roi: TRect): TMat; overload; static;  
    function Mat(const roi: TRect): TMat; overload; 
// ~Mat();
    class operator Finalize(var Dest: TMat);
    class operator assign(var Dest: TMat; const [ref] Src: TMat); 
// Mat& operator = (const MatExpr& expr);
    class operator Implicit(const m: TMatExpr): TMat;
// CV_NODISCARD_STD static Mat diag(const Mat& d);
// Mat diag(int d=0) const; 
    function diag(d: Int = 0): TMat;  
// CV_NODISCARD_STD Mat clone() const;
    function clone: TMat;  
// CV_NODISCARD_STD static MatExpr zeros(int rows, int cols, int type);
    class function zeros(const rows, cols: Int; &type: Int): TMatExpr; overload; static; 
// CV_NODISCARD_STD static MatExpr zeros(Size size, int type);
    class function zeros(const size: TSize; &type: Int): TMatExpr; overload; static; 
// CV_NODISCARD_STD static MatExpr ones(int rows, int cols, int type);
    class function ones(rows: Int; cols: Int; &type: Int): TMatExpr; overload; static; 
// CV_NODISCARD_STD static MatExpr ones(int ndims, const int* sz, int type);
   class function ones(ndims: Int; const sz: pInt; &type: Int): TMat; overload; static; 
// void create(int rows, int cols, int type);
    procedure Create(rows, cols, &type: Int); overload;  
// void create(Size size, int type);
    procedure Create(size: TSize; &type: Int); overload; 
// void addref();
    procedure addref;  
// void release();
    procedure release; 
// bool isContinuous() const;
    function isContinuous: BOOL;  
    // //! returns true if the matrix is a submatrix of another matrix
// bool isSubmatrix() const;
    function isSubmatrix: BOOL;          
// size_t elemSize() const;
    function elemSize: size_t;           
// size_t elemSize1() const;
    function elemSize1: size_t;          
// int type() const;
    function &type: Int;                 
// int depth() const;
    function depth: Int;                 
// int channels() const;
    function channels: Int;              
// size_t step1(int i=0) const;
    function step1(i: Int = 0): size_t;  
// bool empty() const;
    function empty: BOOL;                
// size_t total() const;
    function total: size_t; overload;    
// size_t total(int startDim, int endDim=INT_MAX) const;
    function total(startDim: Int; endDim: Int = INT_MAX): size_t; overload;  
// int checkVector(int elemChannels, int depth=-1, bool requireContinuous=true) const;
    function checkVector(elemChannels: Int; depth: Int = -1; requireContinuous: BOOL = true): Int;  

    class operator LogicalNot(const m: TMat): TMatExpr; 
    function at(i0: Int): T; 
  public const
    MAGIC_VAL       = $42FF0000;
    AUTO_STEP       = 0;
    CONTINUOUS_FLAG = CV_MAT_CONT_FLAG;
    SUBMATRIX_FLAG  = CV_SUBMAT_FLAG;

    MAGIC_MASK = $FFFF0000;
    TYPE_MASK  = $00000FFF;
    DEPTH_MASK = 7;
  public
// int flags;
    flags: Int; 
    // ! the matrix dimensionality, >= 2
// int dims;
    dims: Int; 
    // ! the number of rows and columns or (-1, -1) when the matrix has more than 2 dimensions
// int rows, cols;
    rows, cols: Int; 
    // ! pointer to the data
// uchar* data;
    Data: pUChar; 
    //
    // ! helper fields used in locateROI and adjustROI
// const uchar* datastart;
    datastart: pUChar; 
// const uchar* dataend;
    dataend: pUChar;   
// const uchar* datalimit;
    datalimit: pUChar; 
    //
    // ! custom allocator
// MatAllocator* allocator;
    allocator: pMatAllocator; 
    // ! and the standard allocator
    //
    // ! interaction with UMat
// UMatData* u;
    u: pUMatData; 
    //
// MatSize size;
    size: TMatSize; 
// MatStep step;
    step: TMatStep; 
  end;

Размер записи TMat в точности равен размеру данных класса Mat, более того, если класс в OpenCV не содержит виртуальных функций и удается найти описание всех его полей, в том числе и protected, то мы получаем полную аналогию и доступ ко всем полям класса.

Работа с С++ классами с виртуальными функциями, то есть их вызов через таблицу виртуальных функций тоже не проблема, но это опять же тема отдельного разговора.

В случае, если лень искать поля класса, можно просто в теле record создать

Data: array [0 .. 31] of Byte;

как это было сделано для std::vector и оставлено на «TODO». Любой вектор C++ в памяти занимает 32 байта (хотя может и ошибаюсь).

При создании записи (в месте объявления) вызывается

class operator TMat.Initialize(out Dest: TMat);
begin
  Constructor_Mat(@Dest);
end;

При уничтожении

class operator TMat.Finalize(var Dest: TMat);
begin
  Destructor_Mat(@Dest);
end;

Красота! Но есть нюанс.

Иногда в С++ делается следующая штука

Mat r = Mat(10, 3, CV_8UC3);

т.е. сразу вызывается нужный конструктор.

В Object Pascal конструктор будет вызываться два раза

Var r:TMat := TMat.Mat(10, 3, CV_8UC3);

при объявлении и при присвоении (еще и копирование Mat в class operator Assign), что замедляет работу.

Много восторга принесло работающее в Object Pascal неявное преобразование типов, например, из TMat в TInputArray или цепочка TMat→TMatExpr→TMat→TInputArray. Дизассемблированный код Object Pascal стал очень похож на дизассемблированный код C++.

Taк же спасают record helper. Если в С++ можно в заголовочных файлах наобъявлять структур и типов, а потом спокойно и где угодно их использовать, то в Object Pascal так нельзя. Выход из положения — дополнить функциональность в record helper где-нибудь в конце, после всех объявлений.

TMatHelper = record helper for TMat
  Public
// Mat(int rows, int cols, int type, const Scalar& s);
    class function Mat(rows, cols, &type: Int; const s: TScalar): TMat; overload; static;  
// void copyTo( OutputArray m ) const;
    procedure copyTo(m: TOutputArray); overload;          
// void copyTo( OutputArray m, InputArray mask ) const;
    procedure copyTo(m: TOutputArray; mask: TInputArray); overload; 
    class operator Subtract(const m: TMat; const s: TScalar): TMatExpr; 
// Mat& operator = (const Scalar& s);
    class operator Implicit(const s: TScalar): TMat; 
  end;

4. Неприятной особенностью некоторых функций и функций классов OpenCV является использование std: классов. Если с std::string удалось справиться достаточно легко — создана opencv_delphi454.dll, которая экспортирует

class BODY_API ExportString : public cv::String {};

и дальше аналогично классам OpenCV, то вот с std::vector пришлось повозиться. Сейчас реализован не оптимальный, корявый вариант.

enum VectorType {
         vtMat = 1,                      // vector
         vtRect = 2,                     // vector
         vtPoint = 3,           // vector
         vtVectorMat = 4,  // vector>
         vtVectorRect = 5,  // vector>
         vtVectorPoint = 6 // vector>
};

Ну и, например, для создания

BODY_API void CreateStdVector(void* obj, int vt)
{
  if (vt) {
    switch (vt) {
     case vtMat:
       *(static_cast*>(obj)) = vector();
       break;
     case vtRect:
       *(static_cast*>(obj)) = vector();
       break;
     case vtPoint:
       *(static_cast*>(obj)) = vector();
       break;
     case vtVectorMat:
       *(static_cast>*>(obj)) = vector>();
       break;
     case vtVectorRect:
       *(static_cast>*>(obj)) = vector>();
       break;
     case vtVectorPoint:
       *(static_cast>*>(obj)) = vector>();
       break;
       }
    }
}

Более элегантная реализация отложена в TODO. Если уже существуют решения — подскажите.

Хотя в Object Pascal получилось красиво и это работает.

TStdVector = record
  private
{$HINTS OFF}
    Data: array [0 .. 31] of Byte;
{$HINTS ON}
    class function vt: TVectorType; static;
    function GetItems(const index: UInt64): T;  public
    class operator Initialize(out Dest: TStdVector);
    class operator Finalize(var Dest: TStdVector);
    function size: Int64;

    function empty: BOOL; 
    procedure push_back(const Value: T); 
    property Items[const index: UInt64]: T read GetItems; default;
  end;

Плохо только в 

class function TStdVector.vt: TVectorType;
Var
  TypeName: String;
begin
  TypeName := GetTypeName(TypeInfo(T));
  if SameText('TMat', TypeName) then
    vt := vtMat
  else if SameText('TRect_', TypeName) then
    vt := vtRect
  else if SameText('TPoint_', TypeName) then
    vt := vtPoint
  else if SameText('TStdVector>', TypeName) then
    vt := vtVectorPoint
  else
    Assert(false);
end;

Пока оставлено в TODO.

Заключение

  1. Сложилось впечатление, что OpenCV реализуется немножко сумасшедшими людьми в части организации кода. Иногда разбираешься как там все вызывается, преобразуется и думаешь: «Ну зачем так сложно-то?». В тоже время получившийся результат дает низкий уровень вхождения конечного пользователя. Подаешь на вход функции все, что угодно, а она это кушает и скрыто преобразовывает к нужному виду.

  2. В части создания интерфейса для Object Pascal, в целом, можно сказать, что получается. Не все, конечно, удастся реализовать один в один. Например, итераторы в std: vector — это еще та штучка, да и не хочется их вытаскивать только для того, чтобы реализовать один из вариантов конструктора std: vector. Однако примеры использования на C++ переписанные на Object Pascal очень похожи на оригинал, что позволит портировать большинство готовых конструкций и работающих программ.

  3. Скорее всего код будет еще неоднократно подвергаться рефакторингу в том числе и по способам взаимодействия с OpenCV. Кроме того, переведены доли процента объема OpenCV, а количество строк opencv_world.pas без включений уже перевалило за 4700. Конечно, там много комментариев, но часть из них станет кодом. Наверное, все-таки нужно будет разделять на отдельные модули исходя из деления modules в OpenCV.

  4. В конечном итоге будет разработана надстройка, позволяющая использовать родные для Object Pascal типы и структуры, например, TArray, TList. Хотя в последнем я сомневаюсь — слишком уж он громоздкий. Ну и, конечно, компоненты — нам тоже нужен низкий порог входа.

  5. Нужны советы по поводу организации opencv_delphi454.dll, так как я в С++ не Proficiency. Думаю, что знающим людям станет плохо, если они посмотрят код delphi_export.h.

  6. Ну и я не отказываюсь от помощи и советов по организации всего кода. Работа большая, интересная и чем-то похожа на охоту или рыбалку — азарт.

  7. И что же с мечтой? Скажу так — иногда путь к мечте интересней ее осуществления.

© Habrahabr.ru