اتصالات OpenCV-Python

اتصالات پایتون OpenCV چگونه کار می‌کنند؟

۹ دقیقه مطالعهOpenCV 5.xبه‌روزرسانی:

هدف#

یادگیری:

  • اتصالات (Bindings) پایتون OpenCV چگونه تولید می‌شوند؟
  • چگونه ماژول‌های جدید OpenCV را برای پایتون توسعه دهیم؟

اتصالات OpenCV-Python چگونه تولید می‌شوند؟#

در OpenCV، تمامی الگوریتم‌ها با زبان ++C پیاده‌سازی شده‌اند. اما این الگوریتم‌ها می‌توانند از زبان‌های مختلفی مانند پایتون، جاوا و غیره مورد استفاده قرار گیرند. این قابلیت به واسطه مولدهای اتصال (Bindings Generators) ممکن شده است. این مولدها پلی میان ++C و پایتون ایجاد می‌کنند که کاربران را قادر می‌سازد تا توابع ++C را از طریق پایتون فراخوانی کنند. برای به دست آوردن تصویری کامل از آنچه در پس‌زمینه اتفاق می‌افتد، دانش مناسبی از Python/C API مورد نیاز است. یک نمونه ساده در مورد گسترش توابع ++C به پایتون را می‌توان در مستندات رسمی پایتون [1] یافت. بنابراین گسترش تمام توابع موجود در OpenCV به پایتون از طریق نوشتن دستی توابع بسته‌بند (Wrapper) کاری بسیار زمان‌بر است. به همین دلیل OpenCV این کار را به شیوه‌ای هوشمندانه‌تر انجام می‌دهد: OpenCV این توابع بسته‌بند را به صورت خودکار از روی هدرهای ++C با استفاده از اسکریپت‌های پایتونی که در مسیر modules/python/src2 قرار دارند، تولید می‌کند. در ادامه بررسی خواهیم کرد که آن‌ها چه کاری انجام می‌دهند.

اولاً، فایل modules/python/CMakeFiles.txt یک اسکریپت CMake است که بررسی می‌کند کدام ماژول‌ها باید به پایتون گسترش داده شوند. این اسکریپت به طور خودکار تمام ماژول‌هایی که باید گسترش یابند را بررسی کرده و فایل‌های هدر آن‌ها را جمع‌آوری می‌کند. این فایل‌های هدر حاوی فهرستی از تمام کلاس‌ها، توابع، ثابت‌ها و غیره مربوط به آن ماژول‌های خاص هستند.

ثانیاً، این فایل‌های هدر به یک اسکریپت پایتون به نام modules/python/src2/gen2.py ارسال می‌شوند. این همان اسکریپت تولیدکننده اتصالات پایتون است. این اسکریپت، اسکریپت پایتون دیگری به نام modules/python/src2/hdr_parser.py را فراخوانی می‌کند که اسکریپت تحلیل‌گر هدر (Header Parser) است. این تحلیل‌گر هدر، فایل کامل هدر را به لیست‌های کوچک پایتونی تقسیم می‌کند. بنابراین این لیست‌ها شامل تمام جزئیات مربوط به یک تابع خاص، کلاس و غیره هستند. برای مثال، یک تابع برای به دست آوردن لیستی شامل نام تابع، نوع بازگشتی، آرگومان‌های ورودی، انواع آرگومان‌ها و غیره تجزیه می‌شود. لیست نهایی شامل جزئیات تمام توابع، enumها، ساختارها (structs)، کلاس‌ها و غیره موجود در آن فایل هدر است.

اما تحلیل‌گر هدر تمام توابع/کلاس‌های موجود در فایل هدر را تجزیه نمی‌کند. توسعه‌دهنده باید مشخص کند کدام توابع باید به پایتون صادر (Export) شوند. برای این منظور، ماکروهای خاصی به ابتدای این اعلان‌ها اضافه می‌شوند که تحلیل‌گر هدر را قادر می‌سازند تا توابعی را که باید تجزیه شوند شناسایی کند. این ماکروها توسط توسعه‌دهنده‌ای که تابع مورد نظر را برنامه‌نویسی کرده اضافه می‌شوند. به طور خلاصه، توسعه‌دهنده تصمیم می‌گیرد که کدام توابع باید به پایتون گسترش داده شوند و کدام‌ها نباید گسترش یابند. جزئیات این ماکروها در بخش بعدی ارائه خواهد شد.

بنابراین تحلیل‌گر هدر یک لیست بزرگ نهایی از توابع تجزیه‌شده برمی‌گرداند. اسکریپت مولد ما (gen2.py) برای تمام توابع/کلاس‌ها/enumها/ساختارهای تجزیه‌شده توسط تحلیل‌گر هدر، توابع بسته‌بند (wrapper) ایجاد می‌کند (می‌توانید این فایل‌های هدر را در حین کامپایل در پوشه build/modules/python/ با نام فایل‌های *_pyopencv_generated.h بیابید). اما ممکن است برخی از انواع داده‌های پایه‌ای OpenCV مانند Mat، Vec4i، Size وجود داشته باشند که باید به صورت دستی گسترش یابند. برای مثال، نوع داده Mat باید به آرایه Numpy تبدیل شود، Size باید به یک تاپل از دو عدد صحیح تبدیل گردد و غیره. به طور مشابه، ممکن است برخی ساختارها/کلاس‌ها/توابع پیچیده وجود داشته باشند که باید به صورت دستی گسترش یابند. تمام این توابع بسته‌بند دستی در فایل modules/python/src2/cv2.cpp قرار می‌گیرند.

بنابراین اکنون تنها چیزی که باقی می‌ماند کامپایل این فایل‌های بسته‌بند است که ماژول cv2 را به ما ارائه می‌دهد. بنابراین هنگامی که تابعی مانند res = equalizeHist(img1,img2) را در پایتون فراخوانی می‌کنید، دو آرایه numpy ارسال کرده و انتظار دارید یک آرایه numpy دیگر به عنوان خروجی دریافت کنید. بنابراین این آرایه‌های numpy به cv::Mat تبدیل شده و سپس تابع ()equalizeHist در ++C فراخوانی می‌شود. نتیجه نهایی یعنی res دوباره به یک آرایه Numpy تبدیل خواهد شد. بنابراین به طور خلاصه، تقریباً تمام عملیات در ++C انجام می‌شود که سرعتی تقریباً مشابه با سرعت ++C را برای ما فراهم می‌آورد.

بنابراین این نسخه پایه‌ای از چگونگی تولید اتصالات OpenCV-Python است.

چگونه ماژول‌های جدید را به پایتون گسترش دهیم؟#

تحلیل‌گر هدر بر اساس برخی از ماکروهای بسته‌بند افزوده‌شده به اعلان تابع، فایل‌های هدر را تجزیه می‌کند. ثابت‌های شمارشی (Enumeration constants) به هیچ ماکروی بسته‌بندی نیاز ندارند و به طور خودکار بسته‌بندی می‌شوند. اما باقی توابع، کلاس‌ها و غیره به ماکروهای بسته‌بند نیاز دارند.

توابع با استفاده از ماکروی CV_EXPORTS_W گسترش داده می‌شوند. یک مثال در زیر نشان داده شده است:

CV_EXPORTS_W void equalizeHist( InputArray src, OutputArray dst );

تحلیل‌گر هدر می‌تواند آرگومان‌های ورودی و خروجی را از روی کلمات کلیدی مانند InputArray، OutputArray و غیره متوجه شود. معنای آرگومان‌ها در پایتون حفظ می‌شود: هر آنچه در ++C تغییر کند، در پایتون نیز تغییر خواهد کرد. و برعکس، اشیاء فقط‌خواندنی پایتون در صورتی که به عنوان خروجی استفاده شوند نمی‌توانند توسط OpenCV تغییر کنند؛ چنین وضعیتی باعث بروز استثنا (Exception) در پایتون خواهد شد. گاهی اوقات، پارامترهایی که با ارجاع (Reference) در ++C ارسال می‌شوند ممکن است به عنوان ورودی، خروجی یا هر دو استفاده شوند. ماکروهای CV_OUT و CV_IN_OUT امکان رفع ابهام و تولید اتصالات صحیح را فراهم می‌کنند:

CV_EXPORTS_W void minEnclosingCircle( InputArray points,
                                     CV_OUT Point2f& center, CV_OUT float& radius );

برای کلاس‌های بزرگ نیز از CV_EXPORTS_W استفاده می‌شود. برای گسترش متدهای کلاس، از CV_WRAP استفاده می‌گردد. مشابهاً از CV_PROP برای فیلدهای کلاس استفاده می‌شود:

class CV_EXPORTS_W CLAHE : public Algorithm
{
public:
    CV_WRAP virtual void apply(InputArray src, OutputArray dst) = 0;

    CV_WRAP virtual void setClipLimit(double clipLimit) = 0;
    CV_WRAP virtual double getClipLimit() const = 0;
}

توابع چندریختی (Overloaded functions) را می‌توان با استفاده از CV_EXPORTS_AS گسترش داد. اما ما باید یک نام جدید ارسال کنیم تا هر تابع در پایتون با آن نام فراخوانی شود. مورد تابع انتگرال (integral) را در زیر در نظر بگیرید. سه تابع در دسترس است، بنابراین هر کدام با یک پسوند در پایتون نام‌گذاری می‌شوند. مشابهاً از CV_WRAP_AS می‌توان برای بسته‌بندی متدهای چندریختی استفاده کرد:

//! computes the integral image
CV_EXPORTS_W void integral( InputArray src, OutputArray sum, int sdepth = -1 );

//! computes the integral image and integral for the squared image
CV_EXPORTS_AS(integral2) void integral( InputArray src, OutputArray sum,
                                        OutputArray sqsum, int sdepth = -1, int sqdepth = -1 );

//! computes the integral image, integral for the squared image and the tilted integral image
CV_EXPORTS_AS(integral3) void integral( InputArray src, OutputArray sum,
                                        OutputArray sqsum, OutputArray tilted,
                                        int sdepth = -1, int sqdepth = -1 );

کلاس‌ها/ساختارهای کوچک با استفاده از CV_EXPORTS_W_SIMPLE گسترش می‌یابند. این ساختارها با مقدار (by value) به توابع ++C ارسال می‌شوند. مثال‌ها شامل KeyPoint، Match و غیره هستند. متدهای آن‌ها با CV_WRAP و فیلدهای آن‌ها با CV_PROP_RW گسترش پیدا می‌کنند:

class CV_EXPORTS_W_SIMPLE DMatch
{
public:
    CV_WRAP DMatch();
    CV_WRAP DMatch(int _queryIdx, int _trainIdx, float _distance);
    CV_WRAP DMatch(int _queryIdx, int _trainIdx, int _imgIdx, float _distance);

    CV_PROP_RW int queryIdx; // query descriptor index
    CV_PROP_RW int trainIdx; // train descriptor index
    CV_PROP_RW int imgIdx;   // train image index

    CV_PROP_RW float distance;
};

برخی دیگر از کلاس‌ها/ساختارهای کوچک را می‌توان با استفاده از CV_EXPORTS_W_MAP صادر کرد که در آن به یک دیکشنری محلی پایتون صادر می‌شوند. ()Moments نمونه‌ای از آن است:

class CV_EXPORTS_W_MAP Moments
{
public:
    //! spatial moments
    CV_PROP_RW double  m00, m10, m01, m20, m11, m02, m30, m21, m12, m03;
    //! central moments
    CV_PROP_RW double  mu20, mu11, mu02, mu30, mu21, mu12, mu03;
    //! central normalized moments
    CV_PROP_RW double  nu20, nu11, nu02, nu30, nu21, nu12, nu03;
};

بنابراین این‌ها ماکروهای اصلی گسترش هستند که در OpenCV در دسترس قرار دارند. به طور معمول، توسعه‌دهنده باید ماکروهای مناسب را در موقعیت‌های متناظر خود قرار دهد؛ مابقی کار توسط اسکریپت‌های مولد انجام می‌شود. گاهی اوقات ممکن است موارد استثنایی وجود داشته باشد که در آن اسکریپت‌های مولد نتوانند بسته‌بندها را ایجاد کنند. چنین توابعی باید به صورت دستی مدیریت شوند؛ برای انجام این کار هدرهای توسعه *.pyopencv خود را نوشته و آن‌ها را در زیرپوشه misc/python ماژول خود قرار دهید. اما بیشتر اوقات، کدی که مطابق با دستورالعمل‌های کدنویسی OpenCV نوشته شده باشد به طور خودکار توسط اسکریپت‌های مولد بسته‌بندی خواهد شد.

موارد پیشرفته‌تر شامل ارائه قابلیت‌های اضافی به پایتون است که در رابط ++C وجود ندارد، مانند متدهای اضافی، نگاشت انواع داده، یا ارائه آرگومان‌های پیش‌فرض. ما بعداً نوع داده UMat را به عنوان نمونه‌ای از چنین مواردی در نظر خواهیم گرفت. اولاً، برای ارائه متدهای اختصاصی پایتون، از CV_WRAP_PHANTOM به شیوه‌ای مشابه CV_WRAP استفاده می‌شود، با این تفاوت که هدر متد را به عنوان آرگومان خود دریافت می‌کند و شما باید بدنه متد را در فایل الحاقی *.pyopencv خود ارائه دهید. متدهای ()UMat::queue و ()UMat::context نمونه‌هایی از چنین متدهای فانتوم (شبح) هستند که در رابط ++C وجود ندارند، اما برای مدیریت قابلیت‌های OpenCL در سمت پایتون مورد نیاز هستند. ثانیاً، اگر نوع داده موجود قابلیت نگاشت به کلاس شما را دارد، بسیار ترجیح داده می‌شود که چنین قابلیتی را با استفاده از CV_WRAP_MAPPABLE به همراه نوع مبدأ به عنوان آرگومان آن نشان دهید، به جای اینکه توابع اتصال اختصاصی خود را بسازید. این مورد درباره UMat صدق می‌کند که از Mat نگاشت می‌شود. در نهایت، اگر به یک آرگومان پیش‌فرض نیاز است اما در رابط محلی ++C ارائه نشده است، می‌توانید آن را برای سمت پایتون به عنوان آرگومان CV_WRAP_DEFAULT فراهم کنید. مطابق مثال UMat::getMat در زیر:

class CV_EXPORTS_W UMat
{
public:
    //! Mat is mappable to UMat.
    // You would need to provide `static bool cv_mappable_to(const Ptr<Mat>& src, Ptr<UMat>& dst)`
    CV_WRAP_MAPPABLE(Ptr<Mat>);

    /! returns the OpenCL queue used by OpenCV UMat.
    // You would need to provide the method body in the binder code
    CV_WRAP_PHANTOM(static void* queue());

    //! returns the OpenCL context used by OpenCV UMat
    // You would need to provide the method body in the binder code
    CV_WRAP_PHANTOM(static void* context());

    //! The wrapped method become equivalent to `get(int flags = ACCESS_RW)`
    CV_WRAP_AS(get) Mat getMat(int flags CV_WRAP_DEFAULT(ACCESS_RW)) const;
};