OpenCV拾趣(五)——扩展视频控件

本篇简介

在上一节中,我们实现了具备基本功能的视频控件,并以此为基础实现了一个简易的拍照小工具。

>>点击这里回顾上一节内容

在本节中,我们将对视频控件进行扩展,使其具备图像滤波和标定信息的管理功能。

添加视频滤波管理

在实际应用中比较常见的一种应用场景是:对采集到的图像帧进行处理,并将处理的结果实时展示在所采集的帧上。这时,我们在第三节中设计的MatFilter接口就可以发挥作用了——在基础视频控件中添加视频滤波管理相关的功能,即可很方便地实现对所采集图像帧的实时处理和展示。秉承第三节的设计思路,视滤波的管理大致包含下面的功能:

  • 提供滤波器的容器,即第三节中提到过的滤波链,采集到的视频帧将按照视频链的顺序以此进滤波,并将最终结果显示在控件上;
  • 添加、启用、禁用特滤波器的接口。

依照上面的思路,在视频控件类QCvCamView中添加如下声明:

public:
    void appendFilter(QCvMatFilter* filter);
    void setFilterEnabled(QString name, bool enabled);

protected:
    virtual void execFilters(cv::Mat& inMat, cv::Mat& outMat);

private:
    QList<QCvMatFilter*> m_filters;

具体实现如下:

添加Filter

滤波链的末端添加一个Filter:

void QCvCamView::appendFilter(QCvMatFilter* filter)
{
    filter->setParent(this);
    m_filters.append(filter);
}

这里将新添加的滤波器parent设为本对象,利用Qt自身的对象回收机制,确保生成的滤波器对象内存在控件销毁时也会被释放。

启用/禁用某滤波器

void QCvCamView::setFilterEnabled(QString name, bool enabled)
{
    foreach (QCvMatFilter* filter, m_filters)
    {
        if (filter->name() == name)
        {
            filter->setEnabled(enabled);
        }
    }
}

也就是从滤波链中找出名称(name)相同的滤波器并设置使能(enable)标志位。

执行视频帧滤波

void QCvCamView::execFilters(const cv::Mat& inMat, cv::Mat& outMat)
{
    outMat = inMat.clone();
    foreach (QCvMatFilter* filter, m_filters)
    {
        filter->filter(outMat, outMat);
    }
}

然后在控件的绘制事件(paintEvent)里,在VideoCapture对象读取了当前帧之后,调用上面的execFilters方法即可实现视频帧滤波的执行。

视频帧的滤波管理功能到这里就实现完成了。

添加标定数据管理

在需要与现实场景进行交互的应用中,例如在进行AR绘制、3D场景重建时,作为连接现实世界与图像世界的纽带,相机的参数起着至关重要的作用。因此,我们下一步的扩展即为添加相机标定数据的管理。
相机标定数据可分为两类:

  • 内部参数(intrinsic parameters):包括相机的焦距、相平面中心坐标和扭曲参数等相机本身属性参数。
  • 外部参数(extrinsic parameters):相机相对世界坐标系的位置信息,包括相对于三个坐标轴的旋转和相对于原点的平移。
    基于此,我们首先实现一个封装了这些信息的数据类QCvCamera,其中定义了内参和外参两种数据结构:
// 内部参数
struct CameraIntrinsic
{
    cv::Mat cameraMat;
    cv::Mat distortCoeff;
};

// 外部参数
struct Pose
{
    Pose()
    {
    }

    Pose(const cv::Mat& rotation, const cv::Mat& translation)
    {
        this->rotation = rotation;
        this->translation = translation;
    }
    cv::Mat rotation; // in Quaternion
    cv::Mat translation;
};

其中,内参分为相机矩阵cameraMat和扭曲参数向量distortCoeff,是为了方便使用OpenCV本身提供的标定功能。
基于此,相机标定数据的数据类声明如下:

class QCvCamera : public QObject
{
    Q_OBJECT
  public:
    explicit QCvCamera(QObject* parent = nullptr);

  public:
    void setIntrinsic(const CameraIntrinsic& intrinsic);
    const CameraIntrinsic& intrinsic() const;
    const QList<Pose>& poses() const;
    bool isIntrinsicValid() const;
    bool loadCalibrationData(const QString& fileName);

  public slots:
    void addPose(const cv::Mat& rotation, const cv::Mat& translation);

  private:
    CameraIntrinsic m_intrinsic;
    QList<Pose> m_poses;

简单说明一下:

  • 对于同一个相机而言,不考虑老旧损坏等特殊情况的话,内参数据是基本不会发生变化的;而随着相机位置和角度的调整,外参会经常发生变化,因此这里提供了一个存放多组外参的列表作为这个数据类的成员组件;
  • 除了基础的getter和setter之外,还有一个从文件中读取标定信息的方法:
bool QCvCamera::loadCalibrationData(const QString& fileName)
{
    if (!fileName.isEmpty())
    {
        cv::FileStorage fs(fileName.toStdString().c_str(), cv::FileStorage::READ);
        if (fs.isOpened())
        {
            cv::Mat camMat = cv::Mat_<double>::zeros(3, 3);
            cv::Mat distortMat = cv::Mat_<double>::zeros(5, 1);
            cv::FileNode camMatNode = fs["camera_matrix"];
            if (!camMatNode.empty() && camMatNode.isMap())
            {
                camMatNode >> camMat;
            }
            cv::FileNode distortNode = fs["distortion_coefficients"];
            if (!distortNode.empty() && distortNode.isMap())
            {
                distortNode >> distortMat;
            }
            if (!camMat.empty() && !distortMat.empty())
            {
                m_intrinsic.cameraMat = camMat;
                m_intrinsic.distortCoeff = distortMat;
                return true;
            }
            else
            {
                qWarning() << "Intrinsic config data not valid!";
                return false;
            }
        }
        else
        {
            qWarning() << "Could not open intrinsic config file!";
            return false;
        }
    }
    else
    {
        qWarning() << "Invalid file name!";
        return false;
    }
}

这里使用了OpenCV的持久化工具FileStorage,借助它可以很方便地将cv::Mat格式的数据存储到xml、yml或json格式的文件中,或从相应格式的文件中将数据加载为cv::Mat格式。上面的实现中从标定文件里获取名为"camera_matrix"和"distortion_coefficients"的矩阵作为相机矩阵和扭曲参数向量。

将标定数据类封装完成后,向视频控件添加对应的管理功能实际很简单:将标定数据类以成员组件的形式添加到控件类中。考虑到对图像帧的处理大多在各类滤波器中,因此可以在滤波器的基类QCvMatFilter中也添加标定数据类,以便在进行视频滤波时可以直接获取到相机的标定数据。

基于这个思路,首先在QCvMatFilter类中添加QCvCamera成员组件和setter方法(setCamera方法),这里就不详细展示了;

然后在QCvCamView初始化时创建一个QCvCamera对象,并在appendFilter方法中,为每个新添加进来的滤波器设置标定组件(调用filter的setCamera方法);

最后,为视频控件类添加更新标定信息的updateCalibrarion()方法,传入的参数为标定文件的文件名:

bool QCvCamView::updateCalibrarion(QString fileName)
{
    if (!fileName.isEmpty())
    {
        return m_camera->loadCalibrationData(fileName);
    }
    else
    {
        return false;
    }
}

因为所有滤波器使用的都是和视频控件相同的标定数据对象,所以上面这个方法里标定数据的更新会传递到所有的滤波器中。

如此,相机标定数据的管理功能就实现完成了。对于视频控件的功能扩展也就先进行到这里。

基础框架搭建小结

至此,本系列基础框架的搭建也基本告一段落了。

为了让框架搭建的解说更为直观,除了本节之外,每一节都附上了一个简单的实现案例。本节限于篇幅,就不进行具体使用案例的讲解了,在simpleCV仓库的demos/QCvCommonFilters项目中实现了一些较为通用的滤波器,包括高斯模糊和直方图适配等,有兴趣的朋友可以自行将这些滤波器添加到控件的滤波链中查看滤波效果。

接下来,我们将开始这个系列的第二部分,使用我们搭建的这个基础框架,实现一些较为复杂的案例。

>>返回系列索引

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,223评论 25 707
  • 2340.感恩今天双十一给自己买的书,比原价低了好多,又进账了,太棒了。 2341. 感恩自己这两天遇到事情就疗愈...
    李馨兰阅读 156评论 0 0
  • 604年,堅死,廣繼,用楊素,宇文述,虞世基,廣一兄四弟,均滅之。帝好大喜功,閉言專權,修東都,建晉陽,開運河,巡...
    晚霞消失之时阅读 126评论 0 0
  • 《残冬》 文/紫青藤 在太阳刚刚升起来的时候 不同的颜色,线条 似乎想打破被压抑的气氛 我偷偷留着窗户的一条缝隙 ...
    紫青藤阅读 282评论 0 0