二十一、避免 OpenCV 中的常见陷阱
OpenCV 已经问世超过 15 年了。 它包含许多过时或未优化的实现,是过去遗留下来的。 高级 OpenCV 工程师应该知道如何避免在导航 OpenCV API 时出现基本错误,并看到他们的项目在算法上取得成功。
在这一章中,我们将回顾 OpenCV 的历史发展,以及随着计算机视觉的发展,OpenCV 的框架和算法提供的逐渐增加。 我们将使用这些知识来判断 OpenCV 中是否存在用于我们选择的算法的较新的替代方案。 最后,我们将讨论如何识别和避免在使用 OpenCV 创建计算机视觉系统时出现的常见问题或次优选择。
本章将介绍以下主题:
- OpenCV 与最新一波计算机视觉研究的历史回顾
- 检查某个算法在 OpenCV 中可用的日期,以及它是否是过时的标志
- 解决在 OpenCV 中构建计算机视觉系统的陷阱
OpenCV 从 v1 到 v4 的历史
OpenCV 最初是格雷·布拉德斯基(Gray Bradsky)的创意,他曾在英特尔(Intel)担任计算机视觉工程师,大约在 21 世纪初。 布拉德斯基和一个主要来自俄罗斯的工程师团队在英特尔内部开发了 OpenCV 的第一个版本,然后在 2002 年将其制成开源软件(OSS)的 0.9 版。 布拉德斯基随后转到Willow Garage,与 OpenCV 的前创始成员一起工作。 其中包括 Viktor Eurkhimov、Sergey Molinov、Alexander Shishkov 和 Vadim Pisarevsky(他最终创办了公司ItSeez,该公司于 2016 年被英特尔收购),他们开始支持这个年轻的库作为开源项目。
0.9 版主要使用 CAPI,并且已经支持了图像数据操作函数和像素访问、图像处理、过滤、色彩空间变换、几何和形状分析(例如,形态函数、Hough 变换、轮廓查找)、运动分析、基本机器学习(K-Means,HMM)、相机姿态估计、基本线性代数(SVD、特征分解)等功能。 其中许多功能历久弥新,甚至一直延续到今天的 OpenCV 版本。 版本 1.0 于 2006 年发布,它标志着该库作为开放源码软件和计算机视觉领域的主导力量的开始。 2008 年末,Bradsky 和Adrian Kaehler出版了基于 OpenCV v1.1pre1 的畅销书《学习 OpenCV》,这本书在全球取得了巨大的成功,并在未来几年成为 OpenCV C API 的权威指南。
由于其完备性,OpenCV v1 成为学术和工业应用中非常流行的视觉工作框架,特别是在机器人领域,尽管它在功能提供方面与 v0.9 相差不大。 在 1.0 版(2006 年末)发布后,OpenCV 项目进入了多年的冬眠状态,因为创始团队忙于其他项目,开源社区并没有像几年后那样建立起来。 该项目在 2008 年末发布了 v1.1pre1,增加了一些小功能;然而,OpenCV 作为最知名的视觉库的基础是 2.x 版,它引入了非常成功的C++ API。 2.x 版作为 OpenCV 的稳定分支持续了6 年(2009-2015),直到最近,也就是 2018 年初(最新版本 2.4.13.6 发布于 2018 年 2 月),几乎又晚了10 年。 版本 2.4 发布于 2012 年年中,它有一个非常稳定和成功的 API,持续了三年,并且还引入了非常广泛的特性。
版本 2.x 引入了CMake构建系统,该系统当时也被MySQL项目使用,以配合其完全跨平台的目标。 除了新的 C++ API,v2.x 还引入了模块的概念(在 v2.2 中,大约在 2011 年),这些模块可以根据项目组装的需要单独构建、包含和链接,放弃了 v1.x 的cv
、cvaux
、ml
等。 扩展了 2D 功能套件,以及机器学习功能、内置的人脸识别级联模型、3D 重建功能,最重要的是覆盖了所有Python绑定。 早期对 Python 的投资使 OpenCV 成为当时最好的视觉原型开发工具,现在可能仍然如此。 版本 2.4 于 2012 年年中发布,一直开发到 2018 年,由于担心破坏 API 更改,v2.5 从未发布,只是简单地更名为 v3.0(约 2013 年年中)。 2.4.x 版继续引入更重要的特性,比如Android和iOS支持,CUDA和OpenCL实现,CPU 优化(例如,SSE 和其他 SIMD 架构),以及难以置信的新算法。
3.0 版本于 2015 年底首次发布,社区对此反应冷淡。他们正在寻找一个稳定的 API,因为一些 API 有突破性的变化,不可能临时替换。 标头结构也发生了更改(从opencv2/<module>/<module>.hpp
更改为opencv2/<module>.hpp
),这使得转换变得更加困难。 版本 2.4.11+(2015 年 2 月)提供了工具来弥合两个版本之间的差距,并安装了文档来帮助开发人员过渡到 v3.0(https://docs.opencv.org/3.4/db/dfa/tutorial_transition_guide.html)。 2.x 版保持了很强的影响力,许多包管理系统(例如,Ubuntu 的apt
)仍然作为 OpenCV 的稳定版本提供服务,而 3.x 版则在以非常快的速度前进。
经过多年的共存和规划,2.4.x 版让位于 3.x 版,3.x 版拥有改进的 API(引入了许多抽象和基类),并通过新的透明 API(T-API)改进了对 GPU 的支持,该 API 允许 GPU 代码与常规 CPU 代码互换使用。 为社区贡献的代码建立了一个单独的存储库opencv-contrib
,将其作为 2.4.x 版中的一个模块从主代码中移除,改进了构建稳定性和时序。 另一个重大变化是 OpenCV 中的机器学习支持,它在 2.4 版的基础上进行了极大的改进和修订。 3.x 版还通过 OpenCVHAL(硬件加速层),在 Intel x86(例如,ARM、霓虹灯)之外的 CPU 架构上进行了更好的 Android 支持和优化。OpenCV硬件加速层后来合并到了核心模块中。 OpenCV 中首次出现的深度神经网络在 v3.1(2015 年 12 月)被记录为contrib
模块,近两年后在 v3.3(2017 年 8 月)被升级为核心模块:opencv-dnn
。 在 Intel、Nvidia、AMD 和 Google 的支持下,3.x 版本在优化和与 GPU 和 CPU 架构的兼容性方面带来了巨大的改进,并成为 OpenCV 作为优化的计算机视觉库的标志。
4.0 版标志着 OpenCV 作为当今主要开源项目的成熟状态。 旧的 C API(其中许多函数可以追溯到 v0.9)被放弃,取而代之的是C++ 11成为强制的,这也去掉了库中的cv::String
和cv::Ptr
混合体。 Version 4.0 跟踪了针对 CPU 和 GPU 的进一步优化;然而,最有趣的新增功能是Graph API(G-API)模块。 在 Google 的TensorFlow深度学习类库和 Facebook 的PyTorch取得巨大成功之后,G-API 为 OpenCV 带来了时代精神,支持为计算机视觉构建计算图,在 CPU 和 GPU 上执行异构。 凭借对深度学习技术和机器学习、Python 和其他语言、执行图、交叉兼容性以及广泛提供的优化算法的长期投资,OpenCV 被确立为一个具有非常强大的社区支持的前瞻性项目,这使得它在 15 年后成为现有的领先的开放计算机视觉库。
这本丛书Mastering OpenCV的历史与 OpenCV 作为开源计算机视觉的主要库的发展历史交织在一起。 2012 年发布的第一版基于永久 v2.4.x 分支。 这在 2009-2016 年间主导了 OpenCV 领域。 2017 年发布的第二版欢呼 OpenCV v3.1+在社区中的主导地位(始于 2016 年年中)。 第三版,也就是您现在正在阅读的版本,欢迎于 2018 年 10 月下旬发布的 OpenCV v4.0.0。
OpenCV 与计算机视觉中的数据革命
OpenCV 在计算机视觉的数据革命之前就已经存在了。 在 20 世纪 90 年代末,获取大量数据对于计算机视觉研究人员来说并不是一项简单的任务。 快速上网并不常见,甚至大学和大型研究机构的网络也不是很强。 个人和更大的机构计算机的存储容量有限,不允许研究人员和学生处理大量数据,更不用说拥有这样做所需的计算能力(内存和 CPU)了。 因此,对大规模计算机视觉问题的研究被限制在全球选定的实验室名单中,其中包括麻省理工学院的计算机科学和人工智能实验室(CSAIL)、牛津大学机器人研究小组、卡内基梅隆大学(CMU)机器人研究所和加州理工学院(加州理工学院)。 这些实验室也有资源自己管理大量数据,为当地科学家的工作服务,他们的计算集群足够强大,可以处理这种规模的数据。
然而,本世纪初,这一格局发生了变化。 快速的互联网连接使其成为研究和数据交换的中心,同时,计算和存储能力每年呈指数级增长。 大规模计算机视觉工作的民主化带来了计算机视觉工作的开创性大数据集的创建,如MNIST(1998)、CMU Pie(2000)、Caltech 101(2003)和麻省理工学院的 LabelMe(2005)。 这些数据集的发布也推动了围绕大规模图像分类、检测和识别的算法研究。 计算机视觉中一些最具开创性的工作是由这些数据集直接或间接实现的,例如,LeCun 的手写识别(约 1990 年)、Viola 和 Jones 的级联增强人脸检测器(2001)、Lowe 的SIFT(1999、2004)、Dalal 的猪人分类器(2005),以及更多。
在本世纪头十年的后半段,数据供应量急剧增加,发布了许多大数据集,如Caltech 256(2006)、ImageNet(2009)、CIFAR-10(2009)和Pascal VOC(2010),所有这些数据集在今天的研究中仍然起着至关重要的作用。 随着 2010-2012 年左右深度神经网络的出现,以及Krizhevsky 和 Hinton 的 AlexNet(2012)在 ImageNet 大规模视觉识别(ILSVRC)竞赛中的重大胜利,大数据集成为时尚,计算机视觉世界也发生了变化。 ImageNet 本身已经发展到了惊人的规模(超过 1400 万张照片),其他大数据集也是如此,比如微软的 Coco(2015 年,有 250 万张照片),OpenImages V4(2017 年,只有不到 900 万张照片),以及麻省理工学院的 ADE20K(2017,有近 50 万个对象分割实例)。 最近的这一趋势促使研究人员进行更大范围的思考,与十年前的几十个参数相比,今天处理这类数据的机器学习通常会有数千万个参数(在深度神经网络中)。
OpenCV 早期声名鹊起是因为它内置了 Viola 和 Jones 人脸检测方法,该方法基于一系列增强型分类器,这也是许多人在研究或实践中选择 OpenCV 的原因。 然而,OpenCV 一开始并没有瞄准数据驱动的计算机视觉。 在 v1.0 中,机器学习算法仅有级联 Boosting、隐马尔可夫模型和一些无监督方法(如 K-均值聚类和期望最大化)。 主要集中在图像处理、几何形状和形态分析等方面。2.x 和 3.x 版本为 OpenCV 添加了大量标准的机器学习功能;其中包括决策树、随机森林和梯度增强树、支持向量机(SVM)、Logistic 回归、朴素贝叶斯分类等。 目前看来,OpenCV 不是一个数据驱动的机器学习库,在最近的版本中,这一点变得更加明显。 opencv_dnn
核心模块允许开发人员使用通过外部工具(例如,TensorFlow)学习的模型在 OpenCV 环境中运行,OpenCV 提供图像预处理和后处理。 尽管如此,OpenCV 在数据驱动管道中扮演着至关重要的角色,并且在场景中扮演着有意义的角色。
OpenCV 中的历史算法
当开始处理 OpenCV 项目时,应该了解它的历史过去。 OpenCV 作为一个开源项目已经存在了 15 年以上,尽管其非常敬业的管理团队致力于改善库并保持其相关性,但有些实现比其他实现更过时。 有些 API 是为了向后兼容以前的版本,而另一些则是针对特定的算法环境,所有这些都是在添加较新算法的同时进行的。
任何希望为自己的工作选择最佳性能算法的工程师都应该有工具来查询特定算法,以查看何时添加,以及它的来源是什么(例如,一篇研究论文)。 这并不是说任何新的新的就一定比好,因为一些基本的和较旧的算法性能很好,而且在大多数情况下,各种度量之间存在明显的权衡。 例如,数据驱动的深度神经网络执行图像二值化(将彩色或灰度图像转换为黑白)可能会达到最高的精度。 然而,用于自适应二值阈值的Otsu 方法(1979)非常快,并且在许多情况下执行得相当好。 因此,关键是要知道要求,以及算法的细节。**
*# 如何检查算法何时添加到 OpenCV
要更多地了解 OpenCV 算法,最简单的方法之一就是查看它何时被添加到源代码树中。 幸运的是,OpenCV 作为一个开源项目保留了其代码的大部分历史,并且在各个发布版本中记录了更改。 有几个有用的资源可以访问此信息,如下所示:
- OpenCV 源代码库:https://github.com/opencv/opencv
- OpenCV 更改日志:https://github.com/opencv/opencv/wiki/ChangeLog
- OpenCV 阁楼:https://github.com/opencv/opencv_attic
- OpenCV 文档:https://docs.opencv.org/master/index.html
举个例子,让我们来看看cv::solvePnP(...)
函数中的算法,该函数也是物体(或相机)姿态估计最有用的函数之一。 此功能在 3D 重建管道中大量使用。 我们可以在opencv/modules/calib3d/src/solvepnp.cpp
文件中找到solvePnP
,通过 GitHub 中的搜索功能,我们可以追溯到solvepnp.cpp
在 2011 年 4 月 4 日的首次提交(https://github.com/opencv/opencv/commit/04461a53f1a484499ce81bcd4e25a714488cf600)。
在那里,我们可以看到原始的solvePnP
函数最初驻留在calibrate3d.cpp
中,因此我们也可以追溯该函数。 然而,我们很快就发现该文件没有太多的历史记录,因为它起源于 2010 年 5 月首次提交到新的 OpenCV 存储库。 对阁楼储存库的搜索没有发现任何存在于原始储存库之外的东西。 我们最早的solvePnP
版本是 2010 年 5 月 11 日(https://github.com/opencv/opencv_attic/blob/8173f5ababf09218cc4838e5ac7a70328696a48d/opencv/modules/calib3d/src/calibration.cpp),它看起来是这样的:
void cv::solvePnP( const Mat& opoints, const Mat& ipoints,
const Mat& cameraMatrix, const Mat& distCoeffs,
Mat& rvec, Mat& tvec, bool useExtrinsicGuess )
{
CV_Assert(opoints.isContinuous() && opoints.depth() == CV_32F &&
((opoints.rows == 1 && opoints.channels() == 3) ||
opoints.cols*opoints.channels() == 3) &&
ipoints.isContinuous() && ipoints.depth() == CV_32F &&
((ipoints.rows == 1 && ipoints.channels() == 2) ||
ipoints.cols*ipoints.channels() == 2));
rvec.create(3, 1, CV_64F);
tvec.create(3, 1, CV_64F);
CvMat _objectPoints = opoints, _imagePoints = ipoints;
CvMat _cameraMatrix = cameraMatrix, _distCoeffs = distCoeffs;
CvMat _rvec = rvec, _tvec = tvec;
cvFindExtrinsicCameraParams2(&_objectPoints, &_imagePoints, &_cameraMatrix,
&_distCoeffs, &_rvec, &_tvec, useExtrinsicGuess );
}
我们可以清楚地看到,它是旧 C APIcvFindExtrinsicCameraParams2
的一个简单包装。 此 C API 函数的代码存在于calibration.cpp
(https://github.com/opencv/opencv/blob/8f15a609afc3c08ea0a5561ca26f1cf182414ca2/modules/calib3d/src/calibration.cpp#L1043)中,我们可以验证它,因为它自 2010 年 5 月以来没有更改过。 较新版本的solvePnP
(2018 年 11 月最新提交)增加了更多功能,增加了另一个函数(允许使用随机样本共识(RANSAC))和几种特殊的 PnP 算法,如 EPnP、P3P、AP3P、DLS、UPnP,并且在向函数提供SOLVEPNP_ITERATIVE
标志时还保留了旧的 C API(cvFindExtrinsicCameraParams2
)方法。 经过检查,旧的 C 函数似乎通过在平面对象的情况下找到单应,或者使用DLT 方法,然后执行迭代精化来解决姿势估计问题。
像往常一样,如果直接认为旧的 C 方法不如其他方法,那就大错特错了。 然而,较新的方法确实是在 DLT 方法(可追溯到 20 世纪 70 年代)几十年后提出的方法。 例如,UPnP 方法是由 Penate-Sanchez 等人在2013中提出的。 (2013 年)。 同样,在没有仔细检查手头的特定数据和进行比较研究的情况下,我们无法得出哪种算法在要求(速度、精度、内存等)方面表现最好的结论,尽管我们可以得出结论,计算机视觉研究肯定在从 20 世纪 70 年代到 2010 年代的 40 年中取得了进步。 Penate-Sanchez 等人。 实际上,他们的论文表明,UPnP 在速度和准确性方面都远远好于 DLT,这是基于他们用真实和模拟数据进行的实证研究。 有关如何比较算法选项的提示,请参阅章、、查找作业的最佳 OpenCV 算法、。**
*深入检查 OpenCV 代码应该是严肃的计算机视觉工程师的日常工作。 它不仅揭示了潜在的优化,并通过关注较新的方法来指导选择,而且还可能教会很多关于算法本身的知识。
常见陷阱和建议的解决方案
OpenCV 功能非常丰富,提供了多种解决方案和途径来解决视觉理解问题。 伴随着这种强大的力量,也伴随着艰苦的工作,选择和制作符合项目要求的最好的处理流水线。 拥有多个选项意味着找到精确的最佳性能解决方案几乎是不可能的,因为许多部件是可互换的,并且测试所有可能的选项是我们无法实现的。 这个问题的指数复杂性因输入数据而变得更加复杂;输入数据中更多的未知方差将使我们的算法选择更加不稳定。 换句话说,使用 OpenCV 或任何其他计算机视觉库,仍然是经验和艺术的问题。 对于解决方案的一种或另一种方法的成功的先验直觉是计算机视觉工程师通过多年的经验发展起来的,而且在大多数情况下没有捷径。
然而,也可以选择从别人的经验中学习。 如果你已经买了这本书,很可能意味着你正打算这么做。 在这一部分,我们准备了一份部分清单,列出了我们作为计算机视觉工程师多年工作中遇到的问题。 我们也希望为这些问题提出解决方案,就像我们在自己的工作中使用的那样。 该列表集中于计算机视觉工程中出现的问题;但是,任何工程师都应该知道通用软件和系统工程中的常见问题,我们在这里不会列举这些问题。 在实践中,没有一个系统实现是没有问题、错误或未充分优化的,即使在遵循了我们的列表之后,您也可能会发现还有很多事情要做。
任何工程领域的主要常见陷阱都是进行假设而不是断言。 对于任何工程师来说,如果有测量某物的选项,那么它应该被测量,即使是通过近似值,设定上下限,或者测量一个不同的高度相关的现象。 有关可用于在 OpenCV 中进行测量的度量的一些示例,请参阅第 20 章、第章为作业找到最佳 OpenCV 算法。 最好的决策是基于硬数据和可见性的知情决策;然而,这通常不是工程师的特权。 一些项目需要快速而冷淡的启动,这迫使工程师在没有太多数据或直觉的情况下从头开始快速构建解决方案。 在这种情况下,以下建议可以省去很多悲痛:
-
不比较算法选项:和工程师经常犯的一个陷阱是,根据他们首先遇到的、他们过去做过并且似乎有效的东西,或者有很好的教程(别人的经验)的东西来明确地选择算法。 这被称为锚定或聚焦或认知偏差,这是决策理论中的一个众所周知的问题。重复上一章的话,算法的选择可以在准确性、速度、资源等方面对整个管道和项目的结果产生巨大的影响。 在选择算法时做出不知情的决定不是一个好主意。
- 解决方案:OpenCV 可以通过通用基础 API(如
Feature2D
、DescriptorMatcher
、SparseOpticalFlow
等)或通用函数签名(如solvePnP
和solvePnPRansac
)来帮助您无缝测试不同的选项。 高级编程语言(如 Python)在交换算法方面甚至具有更大的灵活性;然而,在 C++ 中,除了多态性之外,这也是可能的,只需要一些插装代码。 建立管道后,看看如何交换某些算法(例如,特征类型或匹配器类型、阈值技术)或它们的参数(例如,阈值、算法标志),并测量对最终结果的影响。 严格更改参数通常被称为超参数调整,这是机器学习中的标准实践。
- 解决方案:OpenCV 可以通过通用基础 API(如
-
没有对自主开发的解决方案或算法进行单元测试:和程序员通常认为自己的工作没有 bug,并且已经涵盖了所有边缘情况,这是一种谬误。 当涉及到计算机视觉算法时,谨慎行事要好得多,因为在许多情况下,输入空间是非常未知的,因为它的维度高得令人难以置信。 单元测试是确保功能不会因意外输入、无效数据或边缘情况(例如,空图像)而中断并具有优雅降级的优秀工具。
- 解决方案:为代码中任何有意义的函数建立单元测试,并确保覆盖重要部分。 例如,任何读取或写入图像数据的函数都是单元测试的理想候选函数。 单元测试是一段简单的代码,通常使用不同的参数多次调用函数,测试函数处理输入的能力(或能力)。 在 C++ 中工作时,测试框架有很多选择;其中一个框架是 Boost C++ 包 Boost.Test(https://www.boost.org/doc/libs/1_66_0/libs/test/doc/html/index.html)的一部分。 下面是一个例子:
#define BOOST_TEST_MODULE binarization test
#include <boost/test/unit_test.hpp>
BOOST_AUTO_TEST_CASE( binarization_test )
{
// On empty input should return empty output
BOOST_TEST(binarization_function(cv::Mat()).empty())
// On 3-channel color input should return 1-channel output
cv::Mat input = cv::imread("test_image.png");
BOOST_TEST(binarization_function(input).channels() == 1)
}
编译此文件后,它将创建一个可执行文件,该文件将执行测试,如果所有测试都通过,则以状态0
退出,如果有任何测试失败,则以状态1
退出。 通常将此方法与CMake 的CTest(https://cmake.org/cmake/help/latest/manual/ctest.1.html)特性(通过CMakeLists.txt
文件中的ADD_TEST
)混合使用,该特性有助于为代码的许多部分构建测试并根据命令运行它们。
- 不检查数据范围:和计算机视觉编程中的一个常见问题是假定数据的范围,例如浮点像素的范围0,1或字节像素的范围0,255。 真的不能保证这些假设在任何情况下都成立,因为内存块可以容纳任何值。 在尝试写入大于表示值的值时,这些错误产生的问题主要是值饱和;例如,将 325 写入可容纳[0,255]的字节将饱和为 255,从而损失大量精度。 其他潜在问题是预期数据和实际数据之间的差异,例如,预期深度图像的范围是0,2048,结果却看到实际范围是[0,1],这意味着它以某种方式被标准化了。 这可能导致算法性能不佳,或者完全崩溃(设想再次将[0,1]范围除以 2048)。
- 解决方案:检查输入数据范围并确保它符合您的预期。 如果范围不在可接受的范围内,您可以抛出一个
out_of_range
异常(标准库类,详细信息请访问https://en.cppreference.com/w/cpp/error/out_of_range)。 您还可以考虑使用CV_ASSERT
命令检查范围,这将在失败时触发cv::error
异常。
- 解决方案:检查输入数据范围并确保它符合您的预期。 如果范围不在可接受的范围内,您可以抛出一个
- 数据类型、通道、转换和舍入误差:和OpenCV
cv::Mat
数据结构中最令人头疼的问题之一是它没有携带变量类型的数据类型信息。cv::Mat
可以保存任意大小的任何类型的数据(float
、uchar
、int
、short
等等),接收函数在没有检查或约定的情况下无法知道数组中有什么数据。 通道的数量也使问题更加复杂,因为一个数组可以任意容纳任意数量的通道(例如,cv::Mat
可以容纳CV_8UC1
或CV_8UC3
)。 如果没有已知的数据类型,可能会导致不需要此类数据的 OpenCV 函数在运行时出现异常,从而可能导致整个应用崩溃。 在同一输入cv::Mat
上处理多种数据类型的问题可能会导致其他转换问题。 例如,如果我们知道传入的数组包含CV_32F
(通过选中input.type() == CV_32F
),我们可能会input.convertTo(out, CV_8U)
将其“规格化”为uchar
个字符;但是,如果float
数据在[0,1]范围内,则输出转换将在[0,255]图像中全部为 0 和 1,这可能是一个问题。- 解决方案:优先选择
cv::Mat_<>
类型(例如,cv::Mat_<float>
)而不是cv::Mat
来携带数据类型,建立非常明确的变量命名约定(例如,cv::Mat image_8uc1
),测试以确保您期望的类型就是您获得的类型,或者创建一个“规范化”方案,将任何意外的输入类型转换为您希望在函数中使用的类型。 当担心数据类型不确定时,使用try .. catch
块也是一个很好的实践。
- 解决方案:优先选择
-
色彩空间产生的问题:RGB 与感知空间(HSV,Lab*)和技术(YUV):色彩空间是在像素阵列(图像)中以数值编码颜色信息的一种方式。 但是,这种编码存在许多问题。 最重要的问题是,任何颜色空间最终都会变成存储在数组中的一系列数字,并且 OpenCV 不会跟踪
cv::Mat
中的颜色空间信息(例如,一个数组可能包含 3 字节的 RGB 或 3 字节的 HSV,而变量 user 无法区分)。 这不是一件好事,因为我们倾向于认为,我们可以对数字数据进行任何形式的数字操作,这将是有意义的。 然而,在某些色彩空间中,某些操作需要认识到色彩空间。 例如,在非常有用的HSV(色调,饱和度,值)颜色空间中,必须记住,H(色调)实际上是度[0,360]的度量,通常压缩为[0,180]以适合uchar
个字符。 因此,在 H 通道中设置值 200 是没有意义的,因为它违反了颜色空间定义并导致意外问题。 线性运算也是如此。 例如,如果我们希望将图像调暗 50%,则在 RGB 中,我们只需将所有通道除以 2 即可;然而,在 HSV(或 Lab、Luv 等)中,必须仅对V(值)或L(亮度)*通道执行除法。当处理非字节图像(如 YUV420 或 RGB555(16 位色彩空间))时,问题会变得更加严重。 这些图像在位级别(而不是字节级别)存储像素值,在同一字节中合成多个像素或一个通道的数据。 例如,RGB555 像素以两个字节(16 位)存储:一位未使用,然后五位用于红色,五位用于绿色,五位用于蓝色。 在这种情况下,所有类型的数值操作(例如,算术)都会失败,并可能导致数据无法修复的损坏。 * 解决方案:始终了解您处理的数据的色彩空间。 当使用
cv::imread
从文件读取图像时,您可能会认为它们是按BGR顺序读取的(标准 OpenCV 像素数据存储)。 当没有可用的色彩空间信息时,您可以依靠试探法或测试输入。 一般来说,你应该警惕只有两个通道的图像,因为它们很可能是位满的色彩空间。 具有四个通道的图像通常是ARGB或RGBA,添加了一个Alpha 通道,再次引入了一些不确定性。 通过在屏幕上显示通道,可以在视觉上完成感知色彩空间的测试。 位打包问题最严重的原因是处理图像文件、外部库中的内存块或源。 在 OpenCV 中,大多数工作都是在单通道灰度或 BGR 数据上完成的,但在保存到文件或准备图像内存块以在不同的库中使用时,跟踪颜色空间转换非常重要。 请记住,cv::imwrite
需要bgr数据,而不是任何其他格式。 -
精度、速度和资源(CPU、内存)的权衡和优化:和计算机视觉中的大多数问题都需要在计算和资源效率之间进行权衡。 有些算法很快,因为它们在内存中缓存关键数据,查找效率很快;其他算法可能很快,因为它们对输入或输出进行了粗略的近似,这会降低准确性。 在大多数情况下,一种吸引人的特质是以牺牲另一种特质为代价的。 不注意这些权衡,或者过于关注它们,可能会成为一个问题。工程师的一个常见陷阱是围绕优化问题。 存在欠优化或过度优化、过早优化、不必要的优化等等。 在寻求优化算法时,有一种倾向是平等对待所有优化,而实际上通常只有一个罪魁祸首(代码行或方法)导致效率最低。处理算法权衡或优化主要是研究和开发时间的问题,而不是结果的问题。 工程师可能在优化上花费太多或不够的时间,或者在错误的时间进行优化。
- 解决方案:在使用算法之前或同时了解算法。 如果您选择一种算法,请通过测试或至少查看 OpenCV 文档页面来确保您了解其复杂性(运行时和资源)。 例如,当匹配图像特征时,应该知道暴力匹配器
BFMatcher
通常比基于 Flann 的近似匹配器FlannBasedMatcher
慢几个数量级,特别是在预加载和缓存特征是可能的情况下。
- 解决方案:在使用算法之前或同时了解算法。 如果您选择一种算法,请通过测试或至少查看 OpenCV 文档页面来确保您了解其复杂性(运行时和资源)。 例如,当匹配图像特征时,应该知道暴力匹配器
简略的 / 概括的 / 简易判罪的 / 简易的
经过 15 年的酝酿,OpenCV 正在成为一个成熟的计算机视觉库。 在此期间,它见证了许多革命的发生,无论是在计算机视觉世界还是在 OpenCV 社区。
在本章中,我们通过一个实用的视角回顾了 OpenCV 的过去,了解如何更好地使用它。 我们将重点放在一个特别好的实践上,即检查历史 OpenCV 代码以找到算法的起源,以便做出更好的选择。 为了应对丰富的功能和特性,我们还针对使用 OpenCV 开发计算机视觉应用中的一些常见缺陷提出了解决方案。
进一步阅读
有关详细信息,请参阅以下链接:
- OpenCV 更改日志:11-13HTTPS://github.com/openCV/openCV/wiki/changelog
- OpenCV 会议记录:https://github.com/opencv/opencv/wiki/Meeting_notes
- OpenCV 版本:https://github.com/opencv/opencv/releases
- OpenCV Attic Release:OPENCVhttps://github.com/opencv/opencv_attic/releases
- 采访 Gary Bradsky,2011:https://www.youtube.com/watch?v=bbnftjY-_lE**