为Delphi翻译OpenCV头文件的想法来源于最初的一个梦想......
我想做一个类似于机器人系统的东西,带有两个可移动的摄像头,能够跟踪(直接“凝视”)给定物体并确定与物体的距离。那是 2012 年。但由于我更像是一名程序员而不是硬件工程师,这一切都是从当时现有算法的实现开始的。很快就意识到算法及其实现不是目标。目标是一个机器人系统。因此,决定使用现有的图像处理库。但是,不幸的是,在 Object Pascal 上,当时发现的现成算法库无法解决这些任务。
我非常喜欢 OpenCV,然后还有 2.4.2 版本,但当然没有 Object Pascal 支持。其他作者尝试翻译 OpenCV 头文件的尝试并不多,只是作为一个例子,概念。我记得很清楚,如果你想要 Java,它让我很生气——这里是一个端口,一个用于创建接口类的工具,对程序员不可见,将调用转发到 OpenCV,最重要的是,几乎是一对一的Java 中的 C++ 类型的程序。这与 Python 和其他一些语言几乎相同。简而言之,Delphi主义(正常人中女权主义的类似物)接管了。
由于还是OpenCV 2.4.2,加上C(cdecl)x32函数,所以翻译头文件的工作原来是挺简单的……大,但是简单,很庞大,但是简单……例程,但是简单。 .. 只需添加“cdecl; 外部核心库;“ 并重新排列参数名称及其类型。甚至有人尝试创建自己的自动广播工具。当时可用的现成工具制作游戏,然后编辑比手动翻译需要更长的时间。更重要的是,OpenCV 头文件不是“Hello world”——关于如何将作者的想法正确地转换为 Object Pascal 代码,必须做出很多决定。例如,由于 Var 参数返回方式的不同,cvGetSize 函数必须实现为交换寄存器的汇编程序插入。有些事情没有最终确定,例如,将 cvImage 转换为 TBitmap(cvImage2Bitmap 函数)——并非所有可能的图像格式都实现了,但这并不重要。在某个时候,我们有两个人。发表后关于 Habré 的文章,Mikhail Grigoriev (@Sleuthhound) 加入了该项目。一般来说,OpenCV API 的整个范围直到版本 2.4.13 都已翻译。然后“森林阿甘”开启了——我们跑到了这个地方,但我们能不能跑得更远。有 OpenCV + OpenGL、OpenCV + SDL、OpenCV + FFMPEG 的示例。FFMPEG x32 甚至有自己的头文件翻译。找到并翻译了当时互联网上存在的所有使用 OpenCV 的 C 程序示例。添加了在 FPC 中使用所有这些的功能。Delphi的组件已经开发完成,貌似“不是每个人”都能安装。未发布组件的自动安装程序......但为什么呢?
因为 OpenCV 3.0 出来了。那么,你为什么要这样做......类,隐式转换......一般来说 - x64。Object Pascal 当时无法做到这一点。Delphi主义已经进入激进Delphi主义阶段。
有很多尝试将 C++ 类转换为 Object Pascal。起点是在 Delphi 中使用 C++ 对象的文章。它是基于两种思路开发的一个代理dll。在第一种情况下,对 Object Pascal 的调用被简单地转换为对 OpenCV 类函数的调用,在第二种情况下,COM 接口在代理 dll 中开发,其引用返回给调用程序。这些尝试的残余可以在source3目录中看到。这两种方法的结果具有可比性。
最初不喜欢这种方法的主要内容:
需要“包装”所有的类,这是很多的,在这种情况下,非常困难。
OpenCV 库本身对于隐式类型转换和许多其他参数的所有灵活性都完全丧失了。
已经编写了几个工具来自动翻译第一个和第二个选项的 OpenCV 类。在其中一个选项中,llvm 用于解析自己开发的 llvm 头文件到 Object Pascal 的翻译。尝试在 Object Pascal 中从 Java 编写 JNI 的类似物。最终,由于时间不够和需要定期吃饭和睡觉,在 Object Pascal 中重新发明 C++ 的尝试失败了......
好吧,正如机器人所说的(不是操作系统),“谁记得旧的,他的相机会坏掉......”。
我想要从 OpenCV 到 Object Pascal 最灵活地使用 OpenCV 库的功能,包括显式和隐式类型转换、对 OpenCV 本身的隐藏吸引力、标准 Object Pascal 构造的使用以及它们对程序员隐藏的转换C++ 格式。Object Pascal 实现这些想法的一些功能早在 Delphi 2005 中就已经存在(如果我没记错的话):类运算符、隐式、显式。但这还不够。在任何情况下,都需要为 Object Pascal 中的类和记录调用构造函数和析构函数,例如 Init 和 Done。对于接口 - 毕竟,它们必须以某种方式创建。那些。在 C++ 中没有这样的机制,自动创建和销毁自己的结构。
现在它发生了 - 自定义管理记录!此外,具有控制赋值的能力——类操作符Assign。通过调用初始化程序自动创建并通过调用“销毁器”来销毁结构。
阻碍工作的第二件事是在 Object Pascal 和 C++ 类的函数中将参数传递给 x32 的不同方式。我不得不调查每个函数(我们正在讨论 OpenCV 2.4.x 中的类)。随着 OpenCV 3.0 向 x64 的过渡,该问题得到了解决,在 Object Pascal 和 C++ 中具有统一的调用约定(但存在细微差别)。
第三个也是最重要的事情是从 Object Pascal 到 OpenCV 的类函数的调用。事实证明(很久以前),C++ 类的函数可以通过“装饰”名称导入,并且正确调用它们已经是一个技术问题。顺便说一句,中国海豚现在经常使用这种方法。
综合起来,这三个因素已经导致了在 Delphi 中使用 OpenCV 4.5.2 的尝试(例如DOpenCV)的出现,尽管再次作为一个概念,作为一个例子。
所以,一天早上醒来,我清楚地听到鸟鸣声,烧水壶的声音——“是时候了!”。然后还有这些意想不到的非工作日休息日。
现在,关于问题的本质。
1.从DLL中获取所有函数不是问题,例如opencv_world454.dll中有7320个(包括6930个函数和类操作符和390个函数)。装修也不是问题。了解这个类的函数是什么:constructor,destructor,operator也不是问题,我们看文档,或者我们可以通过解码后的名字自己梳理一下(这个比较难)。
例如,构造函数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 调用约定传递的,嗯,几乎。例外之一是按值传递非简单类型。Delphi,所有非简单类型(例如记录),即使是1个字节大小,也作为地址传输,同时在C++类中对于小于等于8的数据按值传输字节作为其值在 64 位寄存器中传输。例如,对于 TRect
在某些情况下,你不能按名称导入(你需要弄清楚,让我们把它留在TODO中),你必须按索引导入,这很糟糕。升级到下一个版本会有问题。
3.曾经,在一本关于C++的聪明书中,说在C++中,类是经过强烈辐射和化学突变的结构(struct)。但说真的,为什么要发明新的东西?我们在编译阶段为C++类的数据分配内存,在它声明的地方,我们调用默认构造函数(如果类声明为不带参数),当不需要时,我们调用析构函数。美丽!
这就是自定义管理记录派上用场的地方。它们的行为非常类似于 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<T>(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 中的类不包含虚函数并且可以找到其所有字段的描述,包括受保护的,那么我们获得完整的类比并访问类的所有字段。
使用带有虚函数的C++类,即通过虚函数表调用它们也不是问题,但这又是一个单独谈话的话题。
如果你懒得找类字段,你可以简单地在记录体中创建
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;
美丽!但有一个细微差别。
有时下面的事情是用C++完成的
Mat r = Mat(10, 3, CV_8UC3);
那些。立即调用所需的构造函数。
在 Object Pascal 中,构造函数将被调用两次
Var r:TMat := TMat.Mat(10, 3, CV_8UC3);
在声明和赋值时(还将 Mat 复制到类运算符 Assign 中),这会使事情变慢。
Object Pascal 中的隐式类型转换令人兴奋,例如,从 TMat 到 TInputArray 或 TMat-> TMatExpr-> TMat-> TInputArray 链。反汇编后的 Object Pascal 代码与反汇编后的 C++ 代码非常相似。
这就是记录助手的救援方式。如果在 C++ 中可以在头文件中声明结构和类型,然后冷静地在任何地方使用它们,那么在 Object Pascal 中这是不可能的。出路是在所有声明之后的最后某处向记录助手添加功能。
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<vector*>(obj)) = vector();
break;
case vtRect:
*(static_cast<vector*>(obj)) = vector();
break;
case vtPoint:
*(static_cast<vector*>(obj)) = vector();
break;
case vtVectorMat:
*(static_cast<vector<vector>*>(obj)) = vector<vector>();
break;
case vtVectorRect:
*(static_cast<vector<vector>*>(obj)) = vector<vector>();
break;
case vtVectorPoint:
*(static_cast<vector<vector>*>(obj)) = vector<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<T>.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 中。
给人的印象是OpenCV在代码的组织上是有点疯狂的人实现的。有时你会弄清楚所有东西是如何在那里调用、转换的,然后你会想:“好吧,为什么这么难?” 同时,结果是终端用户参与率低。你给函数的输入提供你想要的任何东西,它吃掉它并秘密地将它转换成所需的形式。
在为 Object Pascal 创建接口方面,一般来说,我们可以说会发生什么。当然,并非所有事情都可以一对一实现。例如, std::vector 中的迭代器仍然是那个小东西,我不想仅仅为了实现 std::vector 构造函数的变体之一而将它们拉出来。但是,用Object Pascal 重写的C++ 中的使用示例与原始示例非常相似,这将允许移植大多数现成的构造和工作程序。
最有可能的是,代码将进行不止一次重构,包括它与 OpenCV 交互的方式。另外,翻译了OpenCV体积的几分之一,没有包含的opencv_world.pas行数已经超过4700行。当然,注释很多,但其中一些会成为代码。可能,毕竟,需要根据 OpenCV 中的模块划分来划分单独的模块。
最终,将开发一个附加组件,允许使用本地 Object Pascal 类型和结构,例如,TArray
需要有关组织 opencv_delphi454.dll 的建议,因为我不精通 C++。我认为有见识的人如果看到 delphi_export.h 代码会感到难过。
好吧,我不拒绝有关组织所有代码的帮助和建议。这项工作很大,很有趣,有点类似于打猎或钓鱼——令人兴奋。
那么我最初的梦想呢?我会这样说 - 有时通往梦想的道路比实现更有趣。