Qt Designer & Runtime Qt libraries on the service of OpenCV developer, or dragging IplImage on QLabel
Introduction
Like most Qt developers using the OpenCV library, I was interested in the topic of representing an image received from a web camera as a component of visual interface design for Qt Designer.
Break a bunch of information on the net, noticed that most of the articles repeat each other, and finding the “highlight” is too difficult. I think my experience with creating a visual component to represent an image of the OpenCV library for Qt Designer will be useful. In addition, I will share information on how to separate the libraries of design time and code execution time. This approach is very well established in RAD Delphi for operating systems of the Windows family.
Remarks
- The author of coffeesmoke does not “chew” simple truths, methods, and subtleties. The code is clear. Functions do not exceed 20 lines.
- The project and use case are implemented on Qt Creator for Linux. Windows application developers must set the settings in .pro files according to their OS
- The author does not enter into discussions like “otherwise it would be better”. Found the best solution, implement it.
- The author does not answer questions about the settings of Qt Creator and / or Qt Designer. Search engines to help you!
- Fasting is for guidance only and serves to indicate the direction of your thoughts. Where they go, you know better.
Design and implementation
Performance
Let us leave the OpenCV image manipulation library for a short while. Our task is a universal runtime library (code runtime library).
Perhaps what I will show now is used universally. However, I did not meet in the open spaces on the topic of building plug-ins for Qt Designer a similar approach.
The task is not to “drag” the Qt Designera plug-in library into the project, but to manage the runtime library. Obviously, the runtime library can be “framed” with a widget representing the component in the Qt Dessigner palette. The
designer contains a simple QLabel component that can transmit images using the pixmap property . This is enough for our purposes. Turning to the translation of documentation Adding Qt Designer Modules
and the very source of information using the link Using Custom Widgets with Qt Designer. The most useful is information about the location of custom plug-in libraries! now we know the destination path: $$ QTDIR / plugin / designer
Let us leave the wrapping of the new component aside. We will deal directly with the component of the runtime library. Create a dynamically loaded library with the CQtOpenCVImage class of our new widget. Cqtopencvimage.h
header file
#ifndef QTOPENCVIMAGE_H
#define QTOPENCVIMAGE_H
#include
#include
#include
#include
#include "opencv2/opencv.hpp"
#include
/*----------------------------------------------------------------------------*/
class CQtOpenCVImagePrivate;
/*----------------------------------------------------------------------------*/
class
#if defined(QDESIGNER_EXPORT_WIDGETS)
QDESIGNER_WIDGET_EXPORT
#else
Q_DECL_EXPORT
#endif
CQtOpenCVImage : public QWidget
{
Q_OBJECT
Q_PROPERTY(QUrl capture READ getCapture WRITE slot_setCapture)
Q_PROPERTY(QString text READ getLabelText WRITE slot_setLabelText)
Q_PROPERTY(QPixmap pixmap READ getPixmap WRITE slot_setPixmap)
Q_PROPERTY(bool scaledContents READ hasScaledContents WRITE slot_setScaledContents)
Q_PROPERTY(bool grayscale READ isGrayscale WRITE slot_setGrayscale)
Q_PROPERTY(Qt::Alignment alignment READ getAlignment WRITE setAlignment)
public:
explicit
CQtOpenCVImage(QWidget* parent = 0, Qt::WindowFlags f = 0);
virtual
~CQtOpenCVImage();
QUrl& getCapture ();
QString getLabelText () const;
const
QPixmap* getPixmap () const;
const
QImage* getImage () const;
Qt::Alignment getAlignment() const;
void setAlignment(Qt::Alignment);
const
QLabel* getQLabel () const;
bool hasScaledContents() const;
bool isGrayscale() const;
public Q_SLOTS:
void slot_setCapture ( const QUrl& );
void slot_setLabelText ( const QString& );
void slot_setPixmap ( const QPixmap& );
void slot_setImage ( const QImage& );
void slot_setQLabel ( const QLabel& );
void slot_setGrayscale(bool);
void slot_setScaledContents(bool);
Q_SIGNALS:
void signal_Grayscale(bool);
void signal_ImageChanged();
void signal_CaptureChanged();
void signal_PixmapChanged();
void signal_LabelChanged();
void signal_AlignmentChanged();
private:
Q_DISABLE_COPY(CQtOpenCVImage)
Q_DECLARE_PRIVATE(CQtOpenCVImage)
QScopedPointer d_ptr;
};
/*----------------------------------------------------------------------------*/
#endif // QTOPENCVIMAGE_H
Pay attention to the areas marked with a special font
- Including the QtDesigner / QDesignerExportWidget header file ensures that the necessary QDESIGNER_WIDGET_EXPORT macro is included when using the directive in the project file
DEFINES += QDESIGNER_EXPORT_WIDGETS
- The CQtOpenCVImagePrivate class hides internal variables and pointers from the declaration of the CQtOpenCVImage main class . It seems to me that this is correct: you should not overload ads with unnecessary information;
- Declare a single private pointer variable
on the structure of the internal implementation of algorithms and data within the framework of Qt rules. IMHO, "good tone."QScopedPointer
d_ptr;
Our CQtOpenCVImage class provides Qt Designer properties (Q_PROPERTY) for visual image control. Property recording methods are implemented by void slot _ **** (***) slots .
And so, the class of representation of the image obtained from the web (IP) camera inherited from QWidget during visual design gives access to the properties:
- capture - video capture device URL or webcam number;
- text - text inscription. Analogue of the text property QLabel;
- pixmap - a pointer to an object of type QPixmap. An analog of the pixmap QLabel property;
- scaledContents - image scalability. Analogue of the property scaledContents QLabel;
- grayscale - a boolean switch for the color mode of the image output, where true corresponds to grayscale, and false - color (RGB);
- alignment - alignment of the contents of text and pixmap. Analog of alignment property QLabel;
I think that it makes no sense to describe the purpose of the class methods: the name speaks for itself.
Let's move on to the class implementation code (file cqtopencvimage.cpp ).
Consider the hidden class CQtOpenCVImagePrivate , its attributes and methods.
class CQtOpenCVImagePrivate
{
Q_DECLARE_PUBLIC(CQtOpenCVImage)
public:
CQtOpenCVImagePrivate(CQtOpenCVImage* owner);
virtual
~CQtOpenCVImagePrivate();
CQtOpenCVImage* q_ptr; // указатель на экземпляр основного класса
QGridLayout* f_grid_layout; // сетка выравнивания находящихся в ней компонент по всему периметру
QLabel* f_label;// экземпляр типа QLabel для вывода изображения
QUrl f_capture_path;// URL устройства видеозахвата или его номер
QImage* p_qt_image; // Указател на экземпляр изображения типа QImage
CvCapture* p_capture; // Указател на экземпляр устройства видеозахвата в рамках описания OpenCV
IplImage* p_opencv_frame; // Указател на текущий фрем из p_capture
uint f_grayscaled:1; // признак представления изобравжения в градациях серого
void init (); // инициализация данных
void close (); // корекктное закрытие всех указателей
void free_qt_image (); // освобождение памяти экземпляра p_qt_image
void new_qt_image ();// распределение памяти для экземпляра p_qt_image
void free_capture (); // освобождение памяти экземпляра p_capture
void new_capture (); // распределение памяти для экземпляра p_capture
};
Let's move on to the implementation of the methods.
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImagePrivate::init ()
{
Q_ASSERT(q_ptr);
f_grid_layout = new QGridLayout(q_ptr);
Q_ASSERT(f_grid_layout);
f_label = new QLabel(q_ptr/*, Qt::WindowNoState*/);
Q_ASSERT(f_label);
f_grid_layout->addWidget (f_label);
p_qt_image = 0,
p_capture = 0,
p_opencv_frame = 0,
f_grayscaled = 0;
}
/*----------------------------------------------------------------------------*/
inline void
CQtOpenCVImagePrivate::close ()
{
free_qt_image ();
free_capture ();
}
/*----------------------------------------------------------------------------*/
CQtOpenCVImagePrivate::CQtOpenCVImagePrivate(CQtOpenCVImage* owner)
: q_ptr(owner)
{
init ();
}
/*----------------------------------------------------------------------------*/
CQtOpenCVImagePrivate::~CQtOpenCVImagePrivate ()
{
close ();
if(!(f_label->parent ()))
delete f_label;
if(!(f_grid_layout->parent ()))
delete f_grid_layout;
}
/*----------------------------------------------------------------------------*/
inline void
CQtOpenCVImagePrivate::free_qt_image ()
{
if(p_qt_image) {
delete p_qt_image;
p_qt_image = 0;
}
}
/*----------------------------------------------------------------------------*/
inline void
CQtOpenCVImagePrivate::free_capture ()
{
if(p_capture) {
cvReleaseCapture(&p_capture);
p_capture = 0;
}
}
The code is clear and readable. No questions should arise. The constructor’s CQtOpenCVImage * owner parameter represents a pointer to a CQtOpenCVImage object that contains an instance of the hidden class as a variable CQtOpenCVImage :: d_ptr.
In the words of the doctor, the hero of Leonid Bronevoy, from the film “Formula of Love”, “So, I will continue ...”.
Consider the definition of a capture device using the new_capture method .
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImagePrivate::new_capture ()
{
free_capture ();//освобождение устройства захвата
bool b_ok;
int i_index = f_capture_path.toString ().toInt (&b_ok); // определение номера камеры, если это номер, а не URL
/* информация к размышлению:
перезахват устройства, если камера реализована как CGI для вебсервера, возвращающий по одному кадру
if(b_ok) {
p_capture = cvCreateCameraCapture(i_index);
if(p_capture)
if(!p_opencv_frame)
p_opencv_frame = cvQueryFrame (p_capture);
} else {
while((p_capture =cvCaptureFromFile(f_capture_path.toString ().toStdString ().c_str ()))) {
p_opencv_frame = cvQueryFrame (p_capture);
new_qt_image ();
cvWaitKey (1000);
}
}
*/
// да, ну его, этот перезахват. Перезахватим по запросу методами OpenCV.
p_capture =
b_ok ?
cvCreateCameraCapture(i_index) :
cvCaptureFromFile(f_capture_path.toString ().toStdString ().c_str ());
p_opencv_frame = p_capture ? cvQueryFrame (p_capture) : 0; // получение фрейма методом из OpenCV
new_qt_image (); // формирование экземпляра QImage
}
For a detailed introduction to OpenCV, read the Table of Contents
Forming an Image of the QImage Type:
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImagePrivate::new_qt_image ()
{
if(!p_capture) return;
free_qt_image (); // освободим указатель на изображение
if(p_opencv_frame) {
// создадим OpenCV экземпляр изображения согласно параметру оттенка серого
IplImage *_tmp =
f_grayscaled ?
cvCreateImage(
cvSize( p_opencv_frame->width, p_opencv_frame->height ),
IPL_DEPTH_8U,
1
) :
cvCloneImage (p_opencv_frame)
;
try
{
// пребразование цвета в RGB или оттенки серого
cvCvtColor( p_opencv_frame, _tmp, f_grayscaled ? CV_RGB2GRAY : CV_BGR2RGB );
// формирование изображения типа QImage
p_qt_image = new QImage(
(const uchar*)(_tmp->imageData),
_tmp->width,
_tmp->height,
_tmp->widthStep,
(f_grayscaled ? QImage::Format_Indexed8 : QImage::Format_RGB888)
);
emit q_ptr->signal_ImageChanged (); // оповещение о готовности
q_ptr->slot_setPixmap (QPixmap::fromImage (*p_qt_image)); // отрисовать изображение на QLabel
} catch(...) {
// упс... Дрова -- в исходное, пельмени разлепить!
close ();
}
// не забываем прибрать за собой!
cvReleaseImage(&_tmp);
}
}
I think everything is clear with the underwater part of the iceberg.
The implementation of the main class is even simpler. Even too lazy to explain. See for yourself:
/*----------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------*/
CQtOpenCVImage::CQtOpenCVImage(QWidget* parent, Qt::WindowFlags f)
: QWidget(parent, f),
d_ptr(new CQtOpenCVImagePrivate(this))
{
}
/*----------------------------------------------------------------------------*/
CQtOpenCVImage::~CQtOpenCVImage()
{
}
/*----------------------------------------------------------------------------*/
QUrl& CQtOpenCVImage::getCapture ()
{
return (d_func ()->f_capture_path);
}
/*----------------------------------------------------------------------------*/
QString CQtOpenCVImage::getLabelText () const
{
return (d_func ()->f_label->text ());
}
/*----------------------------------------------------------------------------*/
bool CQtOpenCVImage::hasScaledContents() const
{
return d_func ()->f_label->hasScaledContents ();
}
/*----------------------------------------------------------------------------*/
void CQtOpenCVImage::slot_setScaledContents(bool Value )
{
d_func ()->f_label->setScaledContents (Value);
}
/*----------------------------------------------------------------------------*/
bool CQtOpenCVImage::isGrayscale() const
{
return d_func ()->f_grayscaled;
}
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImage::slot_setGrayscale( bool Value )
{
if(d_func ()->f_grayscaled != Value)
{
d_func ()->f_grayscaled = Value;
if(!(d_func ()->p_capture))
d_func ()->new_capture ();
else
d_func ()->new_qt_image ();
emit signal_Grayscale (d_func ()->f_grayscaled);
}
}
/*----------------------------------------------------------------------------*/
void CQtOpenCVImage::slot_setLabelText ( const QString& Value )
{
d_func ()->f_label->setText (Value);
}
/*----------------------------------------------------------------------------*/
void CQtOpenCVImage::slot_setCapture ( const QUrl& Value )
{
// Раскомментировать при необходимости исключения переинициализации захвата
// if(getCapture ().toString () != Value.toString () || !d_func ()->p_opencv_frame)
// {
d_func ()->f_capture_path = Value.toString ().trimmed ();
d_func ()->new_capture ();
emit signal_CaptureChanged ();
// }
}
/*----------------------------------------------------------------------------*/
const QPixmap*
CQtOpenCVImage::getPixmap () const
{
return ((const QPixmap*)(d_func ()->f_label->pixmap ()));
}
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImage::slot_setPixmap ( const QPixmap& Value )
{
d_func ()->f_label->setPixmap (Value);
emit signal_PixmapChanged ();
}
/*----------------------------------------------------------------------------*/
const QImage*
CQtOpenCVImage::getImage () const
{
return(d_func ()->p_qt_image);
}
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImage::slot_setImage ( const QImage& Value )
{
d_func ()->free_qt_image ();
d_func ()->p_qt_image = new QImage(Value);
slot_setPixmap (QPixmap::fromImage (*(d_func ()->p_qt_image)));
emit signal_ImageChanged ();
}
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImage::slot_setQLabel ( const QLabel& Value)
{
d_func ()->f_label->setText (Value.text ());
emit signal_LabelChanged ();
}
/*----------------------------------------------------------------------------*/
Qt::Alignment
CQtOpenCVImage::getAlignment() const
{
return(d_func ()->f_label->alignment ());
}
/*----------------------------------------------------------------------------*/
void
CQtOpenCVImage::setAlignment(Qt::Alignment Value)
{
d_func ()->f_label->setAlignment (Value);
emit signal_AlignmentChanged ();
}
/*----------------------------------------------------------------------------*/
const QLabel*
CQtOpenCVImage::getQLabel () const
{
return ((const QLabel*)(d_func ()->f_label));
}
We form a runtime library. We describe the project file images.pro:
TARGET = QtOpenCVImages
TARGET = $$qtLibraryTarget($$TARGET)
TEMPLATE = lib
CONFIG += debug_and_release
Do not forget to include:
DEFINES += QDESIGNER_EXPORT_WIDGETS
Define the path to the library files:
unix:!symbian {
target.path = $$PWD/../../../../lib
DESTDIR = $$PWD/../../../../lib
INSTALLS += target
}
Add class files:
SOURCES += \
cqtopencvimage.cpp
HEADERS += \
cqtopencvimage.h
Define the paths of the headers of the OpenCV library files and the paths of the project dependencies:
INCLUDEPATH += /usr/include/opencv2
DEPENDPATH += /usr/include/opencv2
Add the OpenCV library (core and highgui) to our project:
#win32:CONFIG(release): LIBS += -L/usr/lib/ -lopencv_core
#else:win32:CONFIG(debug, debug|release): LIBS += -L/usr/lib/ -lopencv_cored
#else:
unix: LIBS += -L/usr/lib/ -lopencv_core
#win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../../../../../usr/lib/release/ -lopencv_highgui
#else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../../../../../usr/lib/debug/ -lopencv_highguid
#win32:CONFIG(release): LIBS += -L/usr/lib/release/ -lopencv_highgui
#else:symbian: LIBS += -lopencv_highgui
#else:
unix: LIBS += -L/usr/lib/ -lopencv_highgui
Dear Windows developers, I “jumped off” the Microsoft needle for a long time and I apologize for commenting on the library paths for your OS. You are not stupid people, you’ll figure it out.
* NIX oids, use the ln -s command to create links to your libraries from a location to a directory /usr/lib
Once you create the link, just rebuild the project, and everything will work fine!
So, we have created a dynamic Qt library of the visual component for displaying a picture obtained from a web camera using OpenCV tools.
Source code: Wu a La (literally)
Design
It is time to connect our runtime library to the plugin class shell for Qt Designer.
Create a plugin library project:
TARGET = QtOpenCVWidgets
TARGET = $$qtLibraryTarget($$TARGET)
TEMPLATE = lib
CONFIG += designer plugin release
DEFINES += QDESIGNER_EXPORT_WIDGETS
SOURCES += \
qtopencvimageplugin.cpp \
../qtopencvwidgets.cpp
HEADERS +=\
qtopencvimageplugin.h \
../qtopencvwidgets.h \
../runtime/images/cqtopencvimage.h
RESOURCES += \
qtopencvimages.qrc
unix:!symbian {
target.path = $$PWD/../../../lib
DESTDIR = $$PWD/../../../lib
INSTALLS += target
}
INCLUDEPATH += /usr/include/opencv2
DEPENDPATH += /usr/include/opencv2
#win32:CONFIG(release, debug|release): LIBS += -L/usr/lib/ -lQtOpenCVImages
#else:win32:CONFIG(debug, debug|release): LIBS += -L/usr/lib -lQtOpenCVImages
#else:symbian: LIBS += -lQtOpenCVImages
#else:
unix: LIBS += -L/usr/lib -lQtOpenCVImages
INCLUDEPATH += $$PWD/../runtime/images
DEPENDPATH += $$PWD/../runtime/images
Looks like a Runtime project, doesn't it? Let's consider some differences:
- The name of the library has changed: TARGET = QtOpenCVWidgets ;
- The CONFIG + = designer plugin release parameter contains plugin and designer tags ;
- The plugin is implemented in two files, one per header and the implementation of qtopencvimageplugin. * Qtopencvwidgets. * <. I>;
There is still a dependency on OpenCV library headers as added dependency on the included cqtopencvimage.h header of our QtOpenCVImages widget ;
The dynamic runtime library libQtOpenCVImages is enabled by a symbolic link from/usr/lib/libQtOpenCVImages.so
indicating its real location in the project directory
We create a Qt Designer plugin widget according to all the canons of Qt: Creating Custom Widgets for Qt Designer ( coffeesmoke , as it is said loudly!):
Qtopencvimageplugin.h
#ifndef QTOPENCVIMAGEPLUGIN_H
#define QTOPENCVIMAGEPLUGIN_H
#include
#include
class QtOpenCVImagePlugin : public QObject, public QDesignerCustomWidgetInterface
{
Q_OBJECT
Q_INTERFACES(QDesignerCustomWidgetInterface)
public:
explicit QtOpenCVImagePlugin(QObject *parent = 0);
QString name() const;
QString includeFile() const;
QString group() const;
QIcon icon() const;
QString toolTip() const;
QString whatsThis() const;
bool isContainer() const;
QWidget* createWidget(QWidget *parent);
void initialize(QDesignerFormEditorInterface *core);
bool isInitialized() const;
QString domXml() const;
signals:
public slots:
private:
bool f_init;
};
#endif // QTOPENCVIMAGEPLUGIN_H
The main idea: declare as Q_INTERFACE, inheriting from QDesignerCustomWidgetInterface and overload the methods of the designer interface class according to your requirements.
qtopencvimageplugin.cpp
#include "qtopencvimageplugin.h"
#include "cqtopencvimage.h"
QtOpenCVImagePlugin::QtOpenCVImagePlugin(QObject *parent) :
QObject(parent),
f_init(false)
{
}
QString
QtOpenCVImagePlugin::name() const
{
return "CQtOpenCVImage";// имя класса виджета
}
QString
QtOpenCVImagePlugin::includeFile() const
{
return QLatin1String("cqtopencvimage.h"); // название включения в файл ui_*.h новой формы
}
QString
QtOpenCVImagePlugin::group() const
{
return tr("OpenCV Widgets"); // название группы отображения на панели компонентов Qt Designer
}
QIcon
QtOpenCVImagePlugin::icon() const
{
return QIcon(":QtOpenCVLogo.png"); // значок виджета
}
QString
QtOpenCVImagePlugin::toolTip() const
{
return QString();
}
QString
QtOpenCVImagePlugin::whatsThis() const
{
return QString();
}
bool
QtOpenCVImagePlugin::isContainer() const
{
return false;
}
QWidget*
QtOpenCVImagePlugin::createWidget(QWidget *parent)
{
return new CQtOpenCVImage(parent); // вот она, реализация экземпляра класса нашего виджета!
}
void
QtOpenCVImagePlugin::initialize(QDesignerFormEditorInterface *core)
{
// установка признака инициализации при помещении на форму
if (f_init) return;
f_init = true;
}
bool
QtOpenCVImagePlugin::isInitialized() const
{
return f_init;
}
QString
QtOpenCVImagePlugin::domXml() const
{
// основные параметры для дизайнера
return "\n"
" \n"
" \n"
" \n"
" 0 \n"
" 0 \n"
" 400 \n"
" 200 \n"
" \n"
" \n"
" \n"
" ";
}
There is very little left: create a common wrapper-container for all our widgets of the qtopencvwidgets.h group
Qt <-> OpenCV
#ifndef QTOPENCVWIDGETS_H
#define QTOPENCVWIDGETS_H
#include
#include
#include
class QtOpenCVWidgets :
public QObject,
public QDesignerCustomWidgetCollectionInterface
{
Q_OBJECT
Q_INTERFACES(QDesignerCustomWidgetCollectionInterface)
public:
explicit QtOpenCVWidgets(QObject *parent = 0);
QList customWidgets() const { return f_plugins; }
private:
QList f_plugins;
};
#endif // QTOPENCVWIDGETS_H
qtopencvwidgets.cpp
#include "qtopencvwidgets.h"
#include "images/qtopencvimageplugin.h"
QtOpenCVWidgets::QtOpenCVWidgets(QObject *parent) :
QObject(parent)
{
f_plugins << new QtOpenCVImagePlugin(this);
}
//Q_DECLARE_INTERFACE(QtOpenCVWidgets, "com.trolltech.Qt.Designer.QtOpenCV")
Q_EXPORT_PLUGIN2(qtopencvwidgetsplugin, QtOpenCVWidgets)
Of interest is the constructor, as follows:
f_plugins << new QtOpenCVImagePlugin(this);
. The next time you add new components to the collection, it will be enough to rewrite the constructor by adding another f_plugins operator << new <Next> Plugin (this);Application
Open Qt Designer and look for our СQtOpenCVImage in the component palette. We place it on a new form. Change capture to the desired URL
192.168.0.20:8080/image.jpg
. In this case, this is the Java applet of the camera server that provides the current requested frame. Put a “tick” on the
grayscale
Source code property : If you downloaded it earlier, do not click on it .
conclusions
- We learned how to create a runtime library for implementing our own class based on the visual components of Qt;
- We learned how to frame the runtime library in the Qt Designer plugin, making applications independent of the designer plugin at runtime;
Applications
Simple test case
Let's create a standard Qt GUI application, open the main form of the window, place our component from it, create some service controls, build and run it.
Change the "Address ..." to the value 0 (built-in video camera) and look at the image changes.
Source code: If you downloaded earlier, do not click on it .
What's next?
- Optimization of the current project;
- Description of the proven visual component for searching the borders of the image using the Canny method: CQtOpenCVCannyWidget based on the CQtOpenCVCanny class;
- The relationship between QtOpenCVImage and QtOpenCVCanny
All the best.