使用 OpenCV4 和 C++ 构建计算机视觉项目(五)

部署运行你感兴趣的模型镜像

原文:Building Computer Vision Projects with OpenCV 4 and C++

协议:CC BY-NC-SA 4.0

十九、使用拼接模块的 iOS 全景图

全景成像从摄影的早期就已经存在了。 在那些古老的时代,大约 150 年前,它被称为画法,它用胶带或胶水仔细地将单个图像组合在一起,重建出全景。 随着计算机视觉的发展,全景拼接成为几乎所有数码相机和移动设备上的便捷工具。 如今,创建全景图很简单,只需在视图中滑动设备或相机,拼接计算就会立即发生,最终展开的场景就可以查看了。 在本章中,我们将使用 OpenCV 的 iOS 预编译库在 iPhone 上实现一个适度的全景图像拼接应用。 我们先来研究一下图像拼接背后的一些数学和理论,然后选择相关的 OpenCV 函数来实现,最后用一个基本的 UI 把它集成到 iOS 应用中。

本章将介绍以下主题:

  • 图像拼接与全景图构建概念简介
  • OpenCV 的图像拼接模块及其功能
  • 构建用于全景捕获的 SWIFT iOS 应用 UI
  • 将 Objective C++ 编写的 OpenCV 组件与 SWIFT 应用集成

技术要求

重新创建本章内容需要以下技术和安装:

  • 运行 MacOS High Sierra v10.13+的 MacOSX 计算机(例如 MacBook、iMac)
  • 运行 iOS v11+的 iPhone 6+
  • Xcode v9+
  • CocoaPods v1.5+:https://cocoapods.org/CocoaPods
  • 通过 CocoaPods 安装 OpenCV V4.0

前面组件的构建说明以及实现本章中所示概念的代码将在附带的代码存储库中提供。

本章的代码可以通过 gihub:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter19 访问。

全景图像拼接方法

全景图本质上是将多幅图像融合成一幅图像。 从多个图像创建全景图的过程涉及许多步骤;其中一些是其他计算机视觉任务所共有的,如下所示:

  • 提取二维特征
  • 基于图像特征的图像对匹配
  • 将图像转换或扭曲为公共边框
  • 使用(混合)图像之间的接缝,以获得较大图像令人愉悦的连续效果

其中一些基本操作在运动结构(SFM)、3D 重建视觉里程计同步定位和测绘(SLAM)中也很常见。 我们已经在第 14 章使用 SfM 模块第 18 章Android 摄像机校准和 AR 使用 ArUco 模块中讨论了其中的一些内容。 以下是全景图创建过程的粗略图像:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/c384956f-9772-4891-b3b2-b129a60e7db4.png

在这一部分中,我们将简要回顾一下特征匹配、摄像机姿态估计和图像扭曲。 实际上,全景拼接有多条路径和多个类,具体取决于输入和所需输出的类型。 例如,如果相机有鱼眼镜头(视角极高),则需要特殊处理。

全景图的特征提取与鲁棒匹配

我们从重叠的图像创建全景图。 在重叠区域中,我们寻找将两个图像配准(对齐)的共同视觉特征。 在 SFM 或 SLAM 中,我们逐帧执行此操作,在帧之间重叠极高的实时视频序列中寻找匹配特征。 然而,在全景图中,我们得到的帧之间有一个很大的运动分量,重叠部分可能只占图像的 10%-20%。 首先提取图像特征,如尺度不变特征变换(SIFT)加速稳健特征(SURF)、面向的 Brief(Orb)等图像特征,然后在全景图中的图像之间进行匹配。 请注意,SIFT 和 SURF 功能受专利保护,不能用于商业目的。 ORB 被认为是一种免费的替代方案,但不是很健壮。

下图显示了提取的特征及其匹配:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/709cb9a8-78bc-4b80-8b28-ff80fba2ed9e.png

仿射约束

对于健壮且有意义的两两匹配,我们通常会应用几何约束。 一个这样的约束可以是仿射变换,这是一种只允许缩放、旋转和平移的变换。 在 2D 中,仿射变换可以在 2 x 3 矩阵中建模:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/3978be4a-cca7-40be-b150-2dd553d7f611.png

为了施加约束,我们寻找一个仿射变换https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/5f30d0c6-0f5a-4d78-a092-52655ad69162.png,它可以最小化来自左https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/783b0546-9051-4c44-beab-ee0ea4dbfb65.png和右https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/a9da41d3-624d-4b50-bc2e-acfb6e0a7860.png图像的匹配点之间的距离(误差)。

随机样本一致性(RANSAC)

在上图中,我们演示了这样一个事实:并非所有的点都符合仿射约束,并且大多数匹配对因不正确而被丢弃。 因此,在大多数情况下,我们使用基于投票的估计方法,如随机样本一致性(RANSAC),即随机选择一组点直接(通过齐次线性系统)求解假设M,然后在所有点之间进行投票以支持或拒绝这一假设。

以下是 RANSAC 的伪算法:

  1. 查找图像i和图像j中的点之间的匹配。
  2. 初始化图像ij之间的转换假设,支持度最小。
  3. 虽然不是融合的,但是:
    1. 选择一小组随机的点对。 对于仿射变换,三对就足够了。
    2. 基于对集合直接计算仿射变换T,例如使用线性方程组计算仿射变换T[T1
    3. 计算支座。 对于整个i,j匹配中的每个点p
    4. 如果支持数大于当前假设的支持,则取T作为新假设。
    5. 可选:如果支持足够大(或不同的中断策略为真),则中断;否则,继续迭代。
  4. 返回最新且支持最好的假设转换。
  5. 另外,返回支持掩码:一个二进制变量,说明匹配中的一个点是否支持最终假设。

算法的输出将提供具有最高支持度的变换,并且支持掩码可用于丢弃不支持的点。 我们还可以推论支持点的数量,例如,如果我们观察到的支持点少于 50%,我们就可以认为这场比赛不好,根本不会尝试匹配这两个图像。

RANSAC 还有其他选择,如**最小中值平方(LMedS)**算法,它与 RANSAC 没有太大区别:它不计算支撑点,而是计算每个变换假设的平方误差中值,最后返回中值误差最小的假设。

单应约束

虽然仿射变换对于拼接扫描的文档很有用(例如,从平板扫描仪拼接),但它们不能用于拼接照片全景图。 对于拼接照片,我们可以使用相同的过程来找到单应,即一个平面和另一个平面之间的变换,而不是仿射变换,它有八个自由度,并以 3x3 矩阵表示,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/e5f479b4-92d9-47d5-adf4-acdee0dee012.png

一旦找到合适的匹配,我们就可以找到图像的排序,以便为全景图对它们进行排序,本质上是为了了解图像之间是如何相互关联的。 在大多数情况下,在全景图中,假设摄影师(相机)静止不动,只绕其轴线旋转,例如,从左向右扫视。 因此,目标是恢复摄影机姿势之间的旋转分量。 如果我们认为输入是纯旋转的,那么单应可以分解以恢复旋转:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/acbd7f4f-4437-4d20-80f8-c2deea6a0925.png。如果我们假设单应最初是由摄像机固有的(校准)、矩阵K、和一个 3x3 旋转矩阵R组成的,如果我们知道K,我们可以恢复R。 本征矩阵可以通过摄像机提前校准来计算,或者可以在全景创建过程中估计。

捆绑平差

当在所有照片之间实现了局部转换时,我们可以在全局步骤中进一步优化我们的解决方案。 这被称为束调整的过程,被广泛地构建为所有重建参数(相机或图像变换)的全局优化。 如果图像之间的所有匹配点都放在相同的坐标框架(例如,3D 空间)中,并且存在跨越两个以上图像的约束,则全局束平差的执行效果最好。 例如,如果一个特征点出现在全景图中的两个以上图像中,则它对于全局优化非常有用,因为它涉及注册三个或更多视图。

大多数捆绑平差方法的目标是使平均重建误差最小化。 这意味着,希望将视图的近似参数(例如相机或图像变换)调整为值,以便重新投影回原始视图上的二维点将以最小的误差对齐。 这可以用数学的方式表示如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/dd2a3827-da00-40e0-a301-4ec659bf4e6c.png

其中我们寻找最佳摄像机或图像变换T,使得原始点XI和重新投影点Proj(Tj,Xi)之间的距离最小。 二进制变量vij标记点i是否可以在图像j中看到,并可能导致错误。 这类优化问题可以用迭代非线性最小二乘法求解,如Levenberg-MarQuardt,因为以前的Proj函数通常是非线性的。

用于全景创建的扭曲图像

如果我们知道图像之间的单应关系,我们就可以应用它们的逆来将所有图像投影到同一平面上。 但是,例如,如果所有图像都投影到第一个图像的平面上,则使用单应性的直接扭曲最终会产生拉伸的外观。 在下图中,我们可以看到使用拼接的单应(透视)扭曲的 4 个图像的拼接,这意味着所有图像都对准到第一个图像的平面,这说明了笨拙的拉伸:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/2adc78d0-78fc-4aad-8d0d-59890b0887ae.png

为了解决这个问题,我们把全景看作是从圆柱体内部看图像,图像被投影到圆柱体的壁上,我们旋转中心的相机。 要实现此效果,我们首先需要将图像扭曲到柱面坐标,就好像圆柱体的圆壁被松开并展平为矩形一样。 下图说明了圆柱形翘曲的过程:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/ba1e4934-4875-4f06-8b66-be3d97437250.png

为了在柱面坐标中包装图像,我们首先应用本征矩阵的逆来获得归一化坐标中的像素。 我们现在假设像素是圆柱体表面上的一个点,该点由高度h和角度θ参数化。 高度h实质上对应于y坐标,而xz(相对于y彼此垂直)存在于单位圆上,因此分别对应于 Sinθ和 Cosθ,Sin。 要获得与原始图像相同像素大小的扭曲图像,我们可以再次应用固有矩阵K;但是,我们可以更改焦距参数f,以影响全景图的输出分辨率。

在柱面扭曲模型中,图像之间的关系变成纯粹的平移关系,实际上由单个参数控制:θ*。要将图像拼接在同一平面上,我们只需找到θs,这只是一个自由度,与为每两个连续图像之间的单应性找到八个参数相比,这是很简单的。 圆柱法的一个主要缺点是,我们假设相机的旋转轴运动与其上方轴完全对齐,并且在其位置上保持静止,这在手持相机中几乎从来不是这样的。 尽管如此,柱面全景图仍能产生非常令人满意的效果。 另一个扭曲选项是球面坐标,它允许在xy轴上拼接图像时有更多选项。*

*# 项目概述

该项目将包括以下两个主要部分:

  • 支持捕捉全景的 iOS 应用
  • OpenCV Objective-用于从图像创建全景图并集成到应用中的 C++ 代码

IOS 代码主要涉及构建 UI、访问摄像头和捕获图像。 然后,我们将重点介绍如何将图像转换为 OpenCV 数据结构,并从stitch模块运行图像拼接功能。

使用 CocoaPods 设置 iOS OpenCV 项目

要开始在 iOS 中使用 OpenCV,我们必须导入为 iOS 设备编译的库。 使用 CocoaPods 很容易做到这一点,CocoaPods 是一个庞大的 iOS 和 MacOS 外部包存储库,它有一个名为pod的方便的命令行包管理器实用程序。

我们首先为 iOS 创建一个空的 Xcode 项目,模板为“Single View App”。 确保选择 SWIFT 项目,而不是 Objective-C 项目。 稍后将添加我们将看到的 Objective-C++ 代码。

在某个目录中初始化工程后,我们在该目录下的终端中执行pod init命令。 这将在目录中创建一个名为Podfile的新文件。 我们需要编辑该文件,使其如下所示:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'OpenCV Stitcher' do
  use_frameworks!
  # Pods for OpenCV Stitcher
  pod 'OpenCV2', '4.0.0.beta'
end

本质上,只要将pod 'OpenCV2', '4.0.0'添加到target,就会告诉 CocoaPods 下载并解压缩我们项目中的 OpenCV 框架。 然后,我们在终端的同一目录下运行pod install,这将设置我们的项目和工作区以包括所有 Pod(在我们的例子中只有 OpenCVv4)。 要开始处理项目,我们打开$(PROJECT_NAME).xcworkspace文件,而不是像 Xcode 项目那样打开.xcodeproject文件。

用于全景捕捉的 iOS UI

在深入研究将图像集合转换为全景图的 OpenCV 代码之前,我们将首先构建一个 UI 来支持轻松捕获一系列重叠图像。 首先,我们必须确保我们可以访问摄像机以及保存的图像。 打开Info.plist文件并添加以下三行:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/0c7e32e8-47e6-47ca-bcda-e86d735880ae.png

要开始构建 UI,我们将创建一个视图,右侧是相机预览的View对象,左侧是重叠的ImageView对象。 ImageView应覆盖摄像机预览视图的某些区域,以帮助指导用户捕获与上一幅图像有足够重叠的图像。 我们还可以在顶部添加几个ImageView实例以显示以前捕获的图像,并在底部添加一个捕获按钮和一个拼接按钮来控制应用流:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/ed59748d-1ae9-4235-8445-14198549eacb.png

要将相机预览连接到预览视图,必须执行以下操作:

  1. 启动捕获会话(AVCaptureSession)
  2. 选择设备(AVCaptureDevice)
  3. 使用来自设备的输入设置捕获会话(AVCaptureDeviceInput)
  4. 添加用于捕获照片的输出(AVCapturePhotoOutput)

当它们被初始化为 ViewController 类的成员时,大多数都可以立即设置。 以下代码显示如何动态设置捕获会话、设备和输出:

class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {

    private lazy var captureSession: AVCaptureSession = {
        let s = AVCaptureSession()
        s.sessionPreset = .photo
        return s
    }()
    private let backCamera: AVCaptureDevice? = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)

    private lazy var photoOutput: AVCapturePhotoOutput = {
        let o = AVCapturePhotoOutput()
        o.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil)
        return o
    }()
    var capturePreviewLayer: AVCaptureVideoPreviewLayer?

其余的初始化可以通过viewDidLoad函数来完成,例如,将捕获输入添加到会话中,并创建用于在屏幕上显示摄像机视频源的预览层。 以下代码显示了初始化过程的其余部分,将输入和输出添加到捕获会话,并设置预览层。

    override func viewDidLoad() {
        super.viewDidLoad()

        let captureDeviceInput = try AVCaptureDeviceInput(device: backCamera!)
        captureSession.addInput(captureDeviceInput)
        captureSession.addOutput(photoOutput)

        capturePreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        capturePreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
        capturePreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait

        // add the preview layer to the view we designated for preview
        let previewViewLayer = self.view.viewWithTag(1)!.layer
        capturePreviewLayer?.frame = previewViewLayer.bounds
        previewViewLayer.insertSublayer(capturePreviewLayer!, at: 0)
        previewViewLayer.masksToBounds = true
        captureSession.startRunning()
    }

设置好预览后,只需单击一下即可处理照片捕获。 下面的代码显示了单击按钮(TouchUpInside)将如何通过delegate触发photoOutput函数,然后简单地将新图像添加到列表中,并将其保存到照片库中的内存中。

@IBAction func captureButton_TouchUpInside(_ sender: UIButton) {
    photoOutput.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
}

var capturedImages = [UIImage]()

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    let cgImage = photo.cgImageRepresentation()!.takeRetainedValue()
    let image = UIImage(cgImage: cgImage)
    prevImageView.image = image // save the last photo, for the overlapping ImageView
    capturedImages += [image] // add to array of captured photos

    // save to photo gallery on phone as well
    PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAsset(from: image)
    }, completionHandler: nil)
}

这将允许我们连续捕捉多幅图像,同时帮助用户将一幅图像与另一幅图像对齐。 以下是在手机上运行的 UI 示例:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/ccb364f9-6abb-4ac4-a5d0-99275e9792ea.png

接下来,我们将了解如何将图像置于 Objective-C++ 上下文中,在该上下文中,我们可以使用 OpenCV C++ API 进行全景拼接。

Objective-C++ 包装器中的 OpenCV 拼接

为了在 iOS 中工作,OpenCV 提供了可以从 Objective-C++ 调用的常用 C++ 接口。 然而,最近几年,苹果鼓励 iOS 应用开发者使用更通用的 SWIFT 语言来构建应用,放弃 Objective-C。 幸运的是,可以很容易地在 SWIFT 和 Objective-C(以及 Objective-C++)之间建立一座桥梁,使我们能够从 SWIFT 调用 Objective-C 函数。 Xcode 自动化了大部分过程,并创建了必要的粘合代码。

首先,我们在 Xcode 中创建一个新文件(Command-N),并选择 Cocoa Touch Class,如以下屏幕截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/898f47aa-4581-4e40-8157-3ffa5ec0c5b2.png

为文件选择一个有意义的名称(例如,StitchingWrapper),并确保选择 Objective-C 作为语言,如以下屏幕截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/acafb684-aec8-46b3-9f6a-889bfca2c705.png

接下来,如以下屏幕截图所示,确认 Xcode 应该为您的 Objective-C 代码创建桥头代码:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/5f6adf49-9ec3-4961-a78c-07c94e5dce39.png

此过程将产生三个文件:StitchingWrapper.hStitchingWrapper.mOpenCV Stitcher-Bridging-Header.h。 我们应该手动将StitchingWrapper.m重命名为StitchingWrapper.mm,以启用 Objective-**C++**而不是普通的 Objective-C。此时,我们准备开始在 Objective-C++ 代码中使用 OpenCV。

在 StitchingWrapper.h 中,我们将定义一个新函数,该函数将接受NSMutableArray*作为前面的 UI SWIFT 代码捕获的图像列表:

@interface StitchingWrapper : NSObject

+ (UIImage* _Nullable)stitch:(NSMutableArray*) images;

@end

而且,在我们的 ViewController 的 SWIFT 代码中,我们可以实现一个函数来处理单击 Stitch 按钮,其中我们从UIImagecapturedImagesSWIFT 数组创建NSMutableArray

@IBAction func stitch_TouchUpInside(_ sender: Any) {
    let image = StitchingWrapper.stitch(NSMutableArray(array: capturedImages, copyItems: true))
    if image != nil {
        PHPhotoLibrary.shared().performChanges({ // save stitching result to gallery
                PHAssetChangeRequest.creationRequestForAsset(from: image!)
        }, completionHandler: nil)
    }
}

回到 Objective-C++ 端,我们首先需要从UIImage*的输入获取 OpenCVcv::Mat对象,如下所示:

+ (UIImage* _Nullable)stitch:(NSMutableArray*) images {
    using namespace cv;

    std::vector<Mat> imgs;

    for (UIImage* img in images) {
        Mat mat;
        UIImageToMat(img, mat);
        if ([img imageOrientation] == UIImageOrientationRight) {
            rotate(mat, mat, cv::ROTATE_90_CLOCKWISE);
        }
        cvtColor(mat, mat, cv::COLOR_BGRA2BGR);
        imgs.push_back(mat);
    }

最后,我们准备对图像数组调用stitching函数,如下所示:

    Mat pano;
    Stitcher::Mode mode = Stitcher::PANORAMA;
    Ptr<Stitcher> stitcher = Stitcher::create(mode, false);
    try {
        Stitcher::Status status = stitcher->stitch(imgs, pano);
        if (status != Stitcher::OK) {
            NSLog(@"Can't stitch images, error code = %d", status);
            return NULL;
        }
    } catch (const cv::Exception& e) {
        NSLog(@"Error %s", e.what());
        return NULL;
    }

使用此代码创建的输出全景示例(请注意柱面扭曲的使用)如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/58c95457-9cec-4fed-b0eb-a8becdfe9033.png

当边缘已混合时,您可能会注意到四个图像之间的照明发生了一些变化。 可以使用cv::detail::ExposureCompensator基础 API 在 OpenCV 图像拼接 API 中处理变化的照明。

简略的 / 概括的 / 简易判罪的 / 简易的

在这一章中,我们学习了全景创作。 我们已经看到了在 OpenCV 的stitching模块中实现的全景创建的一些基本理论和实践。 然后,我们将重点转向创建一个 iOS 应用,该应用可以帮助用户捕捉与重叠视图拼接的全景图像。 最后,我们了解了如何从 SWIFT 应用调用 OpenCV 代码来对捕获的图像运行stitching函数,从而生成完整的全景图。

下一章将重点介绍 OpenCV 算法的选择策略,给出一个手头的问题。 我们将了解如何在 OpenCV 中推理计算机视觉问题及其解决方案,以及如何比较竞争算法以便做出明智的选择。

进一步阅读

里克·塞利斯基关于计算机视觉的书http://szeliski.org/Book/

OpenCV 的图像拼接教程https://docs.opencv.org/trunk/d8/d19/tutorial_stitcher.html

OpenCV 的单应扭曲教程https://docs.opencv.org/3.4.1/d9/dab/tutorial_homography.html#tutorial_homography_Demo5*

二十、为任务寻找最佳 OpenCV 算法

任何计算机视觉问题都可以用不同的方式解决。 根据数据、资源或目标的不同,每种方法都有其优缺点和成功的相对衡量标准。在使用 OpenCV 时,计算机视觉工程师手头有许多算法选项来解决给定的任务。 在知情的情况下做出正确的选择是极其重要的,因为它可以对整个解决方案的成功产生巨大影响,并防止您被束缚在僵化的实现中。 本章将讨论在考虑 OpenCV 中的选项时应遵循的一些方法。 我们将讨论 OpenCV 覆盖的计算机视觉领域,如果存在多个竞争算法,如何在竞争算法之间进行选择,如何衡量算法的成功,最后讨论如何用流水线以稳健的方式衡量成功。

本章将介绍以下主题:

  • 它是否包含在 OpenCV 中? 使用 OpenCV 中提供的算法的计算机视觉主题。
  • 选择哪种算法? OpenCV 中包含多个可用解决方案的主题。
  • 如何知道哪种算法是最好的? 建立衡量算法成功的度量标准。
  • 使用管道在相同的数据上测试不同的算法。

技术要求

本章中使用的技术和安装如下:

  • 具有 Python 绑定的 OpenCV v3 或 v4
  • Jupiter 笔记本服务器

上面列出的组件的构建说明以及实现本章中所示概念的代码将在附带的代码存储库中提供。

本章的代码可以通过 gihub:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter20 访问。

它是否包含在 OpenCV 中?

当第一次处理计算机视觉问题时,任何工程师都应该首先问:我是应该从头开始实施解决方案,还是从纸上或已知方法中实施解决方案,还是使用现有的解决方案并使其适合我的需求?

这个问题与 OpenCV 中提供的实现密切相关。 幸运的是,OpenCV 对规范的和特定的计算机视觉任务都有非常广泛的覆盖范围。 另一方面,并不是所有的 OpenCV 实现都可以轻松地应用于给定的问题。 例如,虽然 OpenCV 提供了一些先进的对象识别和分类功能,但它远远逊于人们在会议和文学中看到的最先进的计算机视觉。 在过去的几年里,当然是在 OpenCV V4.0 中,有一项努力是将深度卷积神经网络与 OpenCV API 轻松集成(通过核心dnn模块),这样工程师就可以享受所有最新和最伟大的工作。

我们努力列出了 OpenCVV4.0 中当前提供的算法,以及对它们对宏伟的计算机视觉主题的覆盖面的主观估计。 我们还注意到 OpenCV 是否提供 GPU 实现覆盖范围,以及该主题是在核心模块还是在 Conrib 模块中涵盖。 Contrib 模块各不相同;有些模块非常成熟,并提供文档和教程(例如,tracking),而另一些模块是黑盒实现,文档非常差(例如,xobjectdetect)。 拥有核心模块实现是一个好兆头,它将会有足够的文档、示例和健壮性。

以下是计算机视觉的主题列表及其在 OpenCV 中的最高级别:

| 主题 | 覆盖范围 | OpenCV 产品 | _ | GPU? |
| 图像加工,图像处理 | 非常好 | 线性和非线性过滤、变换、色彩空间、直方图、形状分析、边缘检测 | 肯定的回答 / 赞成 / 是 | 满意的 / 合格的 / 好的 / 愉快的 |
| 特征检测 | 非常好 | 角点检测、关键点提取、描述符计算 | 是+Conrib | 穷的 / 糟糕的 / 差的 / 可怜的 |
| 分割 | 平庸的 / 平凡的 | 分水岭,轮廓和连通分量分析,二值化和阈值,GrabCut,前景-背景分割,超像素 | 是+Conrib | 穷的 / 糟糕的 / 差的 / 可怜的 |
| 图像对齐、拼接、稳定 | 满意的 / 合格的 / 好的 / 愉快的 | 全景拼接流水线,视频稳定流水线,模板匹配,变换估计,翘曲,无缝拼接 | 是+Conrib | 穷的 / 糟糕的 / 差的 / 可怜的 |
| 从运动到结构 | 穷的 / 糟糕的 / 差的 / 可怜的 | 相机位姿估计、基本和基本矩阵估计、与外部 Sfm 库集成 | 是+Conrib | 毫不 / 绝不 |
| 运动估计、光流、跟踪 | 满意的 / 合格的 / 好的 / 愉快的 | 光流算法、卡尔曼滤波、目标跟踪框架、多目标跟踪 | 主要是人为设计的 | 穷的 / 糟糕的 / 差的 / 可怜的 |
| 立体和三维重建 | 满意的 / 合格的 / 好的 / 愉快的 | 立体匹配框架、三角测量、结构光扫描 | 是+Conrib | 满意的 / 合格的 / 好的 / 愉快的 |
| 摄像机定标 | 非常好 | 从多个模式进行校准,立体声钻机校准 | 是+Conrib | 毫不 / 绝不 |
| 目标检测 | 平庸的 / 平凡的 | 级联分类器、二维码检测器、人脸地标检测器、3D 对象识别、文本检测 | 是+Conrib | 穷的 / 糟糕的 / 差的 / 可怜的 |
| 目标识别、分类 | 穷的 / 糟糕的 / 差的 / 可怜的 | Eigen 和 Fisher 人脸识别,词袋 | 主要是人为设计的 | 毫不 / 绝不 |
| 计算摄影 | 平庸的 / 平凡的 | 去噪、HDR、超分辨率 | 是+Conrib | 毫不 / 绝不 |

虽然 OpenCV 在传统的计算机视觉算法(如图像处理、摄像机校准、特征提取和其他主题)方面做了大量工作,但它对 SfM 和对象分类等重要主题的覆盖率也很低。 在其他方面,如分段,它提供了一个不错的产品,但仍然没有达到最先进的水平,尽管它几乎完全转移到了卷积网络,基本上可以使用dnn模块来实现。

在一些主题中,例如特征检测、提取和匹配,以及摄像机校准,OpenCV 被认为是当今最全面、最免费和最可用的库,可能在成千上万的应用中使用。 然而,在计算机视觉项目的过程中,工程师可能会在原型阶段之后考虑与 OpenCV 解耦,因为库很重,并且大大增加了构建和部署的开销(这对移动应用来说是一个严重的问题)。 在这些情况下,OpenCV 是一个很好的原型制作拐杖,因为它提供了广泛的产品,对测试很有用,并且可以在同一任务的不同算法之间进行选择,例如,计算 2D 特征。 除了原型之外,还有许多其他的考虑因素变得更加重要,比如执行环境、代码的稳定性和可维护性、权限和许可等等。 在那个阶段,使用 OpenCV 应该满足产品的要求,包括前面提到的注意事项。

OpenCV 中的算法选项

OpenCV 有许多涵盖同一主题的算法。 在实现新的处理流水线时,有时流水线中的一个步骤有多种选择。 例如,在第 14 章使用 SfM 模块从运动中探索结构,我们随意决定使用 AKAZE 功能查找图像之间的地标以估计摄像机运动,以及稀疏 3D 结构,然而,在 OpenCV 的features2D模块中有更多类型的 2D 功能可用。 更明智的操作模式应该是根据我们的需要,根据其性能选择要使用的特征算法类型。 至少,我们需要意识到不同的选择。

同样,我们希望创建一种方便的方法来查看同一任务是否有多个选项。 我们创建了一个表,其中列出了在 OpenCV 中具有多个算法实现的特定计算机视觉任务。 我们还努力标记算法是否有共同的抽象 API,从而在代码中轻松且完全可互换。 虽然 OpenCV 为其大多数算法(如果不是全部算法)提供了cv::Algorithm基类抽象,但这种抽象处于非常高的级别,对多态性和互换性的支持很少。 从我们的审查中,我们排除了机器学习算法(ml模块和cv::StatsModel通用 API),因为它们不是适当的计算机视觉算法,以及实际上确实有重叠实现的低级图像处理算法(例如,Hough 检测器系列)。 我们还排除了阴影几个核心主题的 GPU CUDA 实现,例如对象检测、背景分割、2D 功能等等,因为它们大多是 CPU 实现的副本。

以下是 OpenCV 中具有多个实施的主题:

| 主题 | 实施 | 帖子主题:Re:Колибри |
| 光流 | video模块:SparsePyrLKOpticalFlowFarnebackOpticalFlowDISOpticalFlowVariationalRefinement``optflowConrib 模块:DualTVL1OpticalFlowOpticalFlowPCAFlow | 肯定的回答 / 赞成 / 是 |
| 目标跟踪 | track控制模块:TrackerBoostingTrackerCSRTTrackerGOTURNTrackerKCFTrackerMedianFlowTrackerMILTrackerMOSSETrackerTLD外部:DetectionBasedTracker | 是1 |
| 目标检测 | objdetect模块:CascadeClassifierHOGDescriptorQRCodeDetectorlinemodConrib 模块:Detector``arucoConrib 模块:aruco::detectMarkers | 没有Колибри2Колибри |
| 2D 要素 | OpenCV 最成熟的通用 API。features2D模块:AgastFeatureDetectorAKAZEBRISKFastFeatureDetectorGFTTDetectorKAZEMSERORBSimpleBlobDetector``xfeatures2DConrib 模块:BoostDescBriefDescriptorExtractorDAISYFREAKHarrisLaplaceFeatureDetectorLATCHLUCIDMSDDetectorSIFTStarDetectorSURFVGG | 肯定的回答 / 赞成 / 是 |
| 特征匹配 | BFMatcherFlannBasedMatcher | 肯定的回答 / 赞成 / 是 |
| 背景减去 | video模块:BackgroundSubtractorKNNBackgroundSubtractorMOG2``bgsegm控制模块:BackgroundSubtractorCNTBackgroundSubtractorGMGBackgroundSubtractorGSOCBackgroundSubtractorLSBPBackgroundSubtractorMOG | 肯定的回答 / 赞成 / 是 |
| 摄像机定标 | calib3d模块:calibrateCameracalibrateCameraROstereoCalibrate``arucoConrib 模块:calibrateCameraArcuocalibrateCameraCharuco``ccalibConrib 模块:omnidir::calibrateomnidir::stereoCalibrate | 不 / 否决票 / 同 Noh |
| 立体重建 | calib3d模块:StereoBMStereoSGBM``stereoConrib 模块:StereoBinaryBMStereoBinarySGBM``ccalibConrib 模块:omnidir::stereoReconstruct | 部分3 |
| 估算 | solveP3PsolvePnPsolvePnPRansac | 不 / 否决票 / 同 Noh |

1仅适用于trackConrib 模块中的类。
2某些类共享同名函数,但没有继承的抽象类。
3每个模块本身都有一个库,但不能在模块之间共享。

在使用几个算法选项处理问题时,重要的是不要过早地执行一条执行路径。 我们可以使用上表来查看存在的选项,然后探索它们。 接下来,我们将讨论如何从选项池中进行选择。

哪种算法最好?

计算机视觉是一个知识的世界,是一项长达数十年的研究。 与许多其他学科不同,计算机视觉没有很强的层次性或垂直性,这意味着针对给定问题的新解决方案并不总是更好,也可能不是基于之前的工作。 作为一个应用领域,计算机视觉算法的产生关注了以下几个方面,这可能解释了非垂直发展:

  • 计算资源:CPU、GPU、嵌入式系统、内存占用、网络连接。
  • 数据:图像大小、图像数量、图像流(摄像机)数量、数据类型、顺序性、照明条件、场景类型等。
  • 性能要求:实时输出或其他定时限制(例如,人的感知)、准确性和精确度。
  • 元算法:算法简单性(交叉引用 Occam‘s Razor 定理)、实现系统和外部工具、形式证明的可用性。

由于每个算法都是为了迎合其中某个考虑因素而创建的,如果不正确测试其中的一些或全部,就永远无法确定它是否会优于所有其他算法。 诚然,测试给定问题的所有算法是不切实际的,即使实现确实是可用的,而且 OpenCV 肯定有很多实现可用,正如我们在上一节中所看到的。 另一方面,如果计算机视觉工程师不考虑他们的算法选择导致他们的实现不是最优的可能性,那么他们就是玩忽职守。 这在本质上源于没有免费午餐定理,该定理概括地说,在整个可能的数据集空间中,没有一个算法是最好的算法。

因此,在承诺选择其中最好的算法选项之前,测试一组不同的算法选项是一种非常受欢迎的做法。 但是我们如何找到最好的呢? 单词BEST意味着每个人都会比其他人好*(或),这反过来又意味着有一个客观的量表或衡量标准,在这个标准或衡量标准中,他们都被重新评分并按顺序排序。 显然,对于所有问题中的所有算法,没有单一的度量(度量),每个问题都会有自己的度量。 在许多情况下,成功的度量将形成对错误的度量,即与已知的基本事实值的偏差,该值来自人类或我们可以信任的其他算法。 在优化中,这被称为损失函数或成本函数,我们希望最小化(有时最大化)该函数,以便找到得分最低的最佳选项。 另一类重要的度量不太关心输出性能(例如,错误),而更关心运行时计时、内存占用、容量和吞吐量等。*

*以下是我们在部分计算机视觉问题中可能会看到的指标的部分列表:

| 任务 | 示例指标 |
| 重建、配准、特征匹配 | 平均绝对误差(MAE),
均方误差(MSE),
均方根误差(RMSE),
距离平方和(SSD) |
| 目标分类、识别 | 准确率、精确度、召回率、F1 得分、
假阳性率(FPR) |
| 分割,目标检测 | 并集交集(借条) |
| 特征检测 | 可重复性,
精度召回
|

为什么要为给定的任务找到最佳算法,要么是在测试场景中设置我们可以使用的所有选项,并根据选择的指标衡量它们的性能,要么是在标准实验或数据集上获得其他人的测量结果。 应该选择排名最高的选项,其中排名是从多个指标的组合中得出的(在只有一个指标的情况下,这是一项简单的任务)。 接下来,我们将尝试这样一个任务,并在最佳算法上做出知情选择。

算法比较性能测试实例

例如,我们将设置一个场景,要求我们对齐重叠的图像,就像在全景或航空照片拼接中所做的那样。 我们需要测量性能的一个重要特性是具有基本事实,这是我们试图用近似方法恢复的真实条件的精确测量。 地面真实数据可以从提供给研究人员用于测试和比较他们的算法的数据集中获得;事实上,许多这样的数据集都存在,并且计算机视觉研究人员一直在使用它们。 寻找计算机视觉数据集的一个很好的资源是又一个计算机视觉索引到数据集(YACVID),或者https://riemenschneider.hayko.at/vision/dataset/,它在过去八年中一直在积极维护,包含成百上千个到数据集的链接。 以下也是一个很好的数据来源:https://github.com/jbhuang0604/awesome-computer-vision#datasets

然而,我们将选择一种不同的方式来获取基本事实,这在计算机视觉文献中得到了很好的实践。 我们将在我们的参数控制中创造一个人为的情况,并创建一个基准,我们可以改变这个基准来测试我们算法的不同方面。 在我们的示例中,我们将获取单个图像,并将其分割为两个重叠的图像,然后对其中一个图像应用一些变换。 使用我们的算法对图像进行融合将试图重建原始的融合图像,但它可能不会做得很完美。 我们在系统中选择片段时所做的选择(例如,2D 特征的类型、特征匹配算法和变换恢复算法)将影响最终结果,我们将对其进行测量和比较。 通过使用人造地面真实数据,我们可以很好地控制试验的条件和水平。

考虑下图及其双向重叠拆分:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/6d0b9b5b-3892-4e5e-a4ec-25c50b316c36.png

Image: https://pixabay.com/en/forest-forests-tucholski-poland-1973952/

我们保持左边的图像不变,同时对右边的图像执行人工变换,看看我们的算法能够多好地撤销它们。 为简单起见,我们将只旋转几个括号中的右侧图像,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/540477d2-92a2-4b1b-ba3a-63c196154b26.png

我们为无旋转的情况添加了一个中间括号,在这种情况下,右侧的图像只被稍微平移了一些。“这构成了我们的地面真实数据,其中我们确切地知道发生了什么变换以及原始输入是什么。

我们的目标是衡量不同 2D 特征描述符类型在对齐图像方面的成功程度。 衡量我们是否成功的一个标准可以是最终重新拼接图像像素上的均方误差(MSE)。 如果转换恢复做得不是很好,像素将不会完全对齐,因此我们预计会看到很高的 MSE。 当 MSE 接近零时,我们知道拼接做得很好。 出于实际原因,我们可能还想知道哪个特性是最有效的,这样我们还可以测量执行时间。 为此,我们的算法可以非常简单:

  1. 将原始图像分割为左图像右图像
  2. 对于每种要素类型(SURF、SIFT、ORB、AKAZE、BRISK),请执行以下操作:
    1. 在左侧图像中查找关键点和特征。
    2. 对于每个旋转角度[-90,-67,…,67,90],请执行以下操作:
      1. 按旋转角度旋转右侧图像。
      2. 在旋转的右侧图像中查找关键点和特征。
      3. 在旋转的右图像和左图像之间匹配关键点。
      4. 估计刚性 2D 变换。
      5. 根据估计进行变换。
      6. 使用原始未分割图像测量最终结果的MSE
      7. 测量提取、计算和匹配要素并执行对齐所需的总时间

作为一种快速优化,我们可以缓存旋转的图像,而不计算每个特征类型的图像。 算法的其余部分保持不变。 此外,为了在时间上保持公平,我们应该注意为每种特征类型提取相似数量的关键点(例如,2500 个关键点),这可以通过设置关键点提取函数的阈值来实现。

注对齐执行管道与特征类型无关,并且在给定匹配关键点的情况下工作方式完全相同。 这是测试许多选项的一个非常重要的功能。 使用 OpenCV 的cv::Feature2Dcv::DescriptorMatcher公共基础 API 可以实现这一点,因为所有功能和匹配器都实现它们。 但是,如果我们看一下*中的表格,它是否包含在 OpenCV 中?*一节中,我们可以看到这可能不适用于 OpenCV 中的所有视觉问题,因此我们可能需要添加我们自己的仪器代码来进行此比较。

在附带的代码中,我们可以找到该例程的 Python 实现,它提供以下结果。 为了测试旋转不变性,我们改变角度并测量重建的均方误差:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/d5e4e88c-eecf-45bb-a54f-02f22b646bf2.png

对于相同的实验,我们记录了一种特征类型的所有实验的平均 MSE,以及平均执行时间,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/build-cv-proj-opencv4-cpp/img/ae41a5e8-accc-4d4f-a2ba-3bd8586b201f.png

结果分析,我们可以清楚地看到一些功能在 MSE 方面表现更好,无论是在不同的旋转角度还是整体上,我们还可以看到时间上的很大差异。 在旋转角度范围内,AKAZE 和 SURF 的对准成功率似乎是最高的,AKAZE 在更高的旋转角度(~60°)时更具优势。 然而,在角度变化非常小的情况下(旋转角接近 0°),SIFT 在 MSE 接近于零的情况下实现了几乎完美的重建,它的重建效果也不亚于旋转在 30°以下的其他图像。 ORB 在整个领域做得非常糟糕,虽然 Bliisk 没有那么糟糕,但很少能够击败任何先行者。

考虑到时间,ORB 和 BRISK(本质上是相同的算法)是明显的赢家,但它们在重建精度方面都远远落后于其他算法。 AKAZE 和 SURF 是计时性能不相上下的领先者。

现在,由我们作为应用开发人员根据项目的要求对功能进行排序。 根据我们执行的这次测试的数据,应该很容易做出决定。 如果我们在寻找速度,我们会选择轻快,因为它是最快的,而且性能比 ORB 更好。 如果我们追求精确度,我们会选择 AKAZE,因为它是最好的表演者,而且比冲浪更快。 使用 SURF 本身就是一个问题,因为算法不是免费的,而且它是受专利保护的,所以我们很幸运地发现 AKAZE 是一个免费和足够的替代方案。

这是一个非常初级的测试,只看了两个简单的测量(MSE 和时间)和一个变化的参数(旋转)。 在实际情况中,我们可能希望根据系统的要求在转换中加入更多的复杂性。 例如,我们可以使用完全透视变换,而不仅仅是刚性旋转。 此外,我们可能希望对结果进行更深入的统计分析。 在此测试中,对于每个旋转条件,我们只运行一次对齐过程,这不利于捕获良好的计时度量,因为某些算法可能受益于连续执行(例如,将静态数据加载到内存)。 如果我们有多次执行,我们可以对执行中的差异进行推理,并计算标准差或误差,以便为我们的决策过程提供更多信息。 最后,给定足够的数据,我们可以执行统计推断过程和假设检验,例如t 检验方差分析(ANOVA),以确定条件之间的细微差别(例如,AKAZE 和 SURF)是否具有统计显著性,或者太过嘈杂而无法区分。

简略的 / 概括的 / 简易判罪的 / 简易的

为这项工作选择最好的计算机视觉算法是一个虚幻的过程,这是许多工程师不执行它的原因。 虽然针对不同选择发布的调查工作提供了基准性能,但在许多情况下,它没有对工程师可能遇到的特定系统需求进行建模,因此必须实施新的测试。 测试算法选项的主要问题是检测代码,这对工程师来说是一项额外的工作,而且并不总是那么简单。 OpenCV 为几个视觉问题领域的算法提供了基础 API,但覆盖年限并不完整。 另一方面,OpenCV 覆盖了非常广泛的计算机视觉问题,是执行此类测试的主要框架之一。

在选择算法时做出明智的决定是视觉工程的一个非常重要的方面,有许多要素需要优化,例如,速度、准确性、简单性、内存占用,甚至可用性。 每个视觉系统工程都有特定的要求,这些要求会影响每个元素的权重,从而影响最终的决策。 通过相对简单的 OpenCV 代码,我们了解了如何收集数据、绘制图表,并就玩具问题做出明智的决定。

在下一章中,我们将讨论 OpenCV 开源项目的历史,以及使用 OpenCV 时的一些常见陷阱以及针对这些陷阱提出的解决方案。*

二十一、避免 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 的cvcvauxml等。 扩展了 2D 功能套件,以及机器学习功能、内置的人脸识别级联模型、3D 重建功能,最重要的是覆盖了所有Python绑定。 早期对 Python 的投资使 OpenCV 成为当时最好的视觉原型开发工具,现在可能仍然如此。 版本 2.4 于 2012 年年中发布,一直开发到 2018 年,由于担心破坏 API 更改,v2.5 从未发布,只是简单地更名为 v3.0(约 2013 年年中)。 2.4.x 版继续引入更重要的特性,比如AndroidiOS支持,CUDAOpenCL实现,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::Stringcv::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)、C****MU 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 作为一个开源项目保留了其代码的大部分历史,并且在各个发布版本中记录了更改。 有几个有用的资源可以访问此信息,如下所示:

举个例子,让我们来看看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(如Feature2DDescriptorMatcherSparseOpticalFlow等)或通用函数签名(如solvePnPsolvePnPRansac)来帮助您无缝测试不同的选项。 高级编程语言(如 Python)在交换算法方面甚至具有更大的灵活性;然而,在 C++ 中,除了多态性之外,这也是可能的,只需要一些插装代码。 建立管道后,看看如何交换某些算法(例如,特征类型或匹配器类型、阈值技术)或它们的参数(例如,阈值、算法标志),并测量对最终结果的影响。 严格更改参数通常被称为超参数调整,这是机器学习中的标准实践。
  • 没有对自主开发的解决方案或算法进行单元测试程序员通常认为自己的工作没有 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](unsigned charCV_8U)。 真的不能保证这些假设在任何情况下都成立,因为内存块可以容纳任何值。 在尝试写入大于表示值的值时,这些错误产生的问题主要是值饱和;例如,将 325 写入可容纳[0,255]的字节将饱和为 255,从而损失大量精度。 其他潜在问题是预期数据和实际数据之间的差异,例如,预期深度图像的范围是[0,2048](例如,2 米(毫米)),结果却看到实际范围是[0,1],这意味着它以某种方式被标准化了。 这可能导致算法性能不佳,或者完全崩溃(设想再次将[0,1]范围除以 2048)。

    • 解决方案:检查输入数据范围并确保它符合您的预期。 如果范围不在可接受的范围内,您可以抛出一个out_of_range异常(标准库类,详细信息请访问https://en.cppreference.com/w/cpp/error/out_of_range)。 您还可以考虑使用CV_ASSERT命令检查范围,这将在失败时触发cv::error异常。
  • 数据类型、通道、转换和舍入误差OpenCVcv::Mat数据结构中最令人头疼的问题之一是它没有携带变量类型的数据类型信息。 cv::Mat可以保存任意大小的任何类型的数据(floatucharintshort等等),接收函数在没有检查或约定的情况下无法知道数组中有什么数据。 通道的数量也使问题更加复杂,因为一个数组可以任意容纳任意数量的通道(例如,cv::Mat可以容纳CV_8UC1CV_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 像素数据存储)。 当没有可用的色彩空间信息时,您可以依靠试探法或测试输入。 一般来说,你应该警惕只有两个通道的图像,因为它们很可能是位满的色彩空间。 具有四个通道的图像通常是ARGBRGBA,添加了一个Alpha 通道,再次引入了一些不确定性。 通过在屏幕上显示通道,可以在视觉上完成感知色彩空间的测试。 位打包问题最严重的原因是处理图像文件、外部库中的内存块或源。 在 OpenCV 中,大多数工作都是在单通道灰度或 BGR 数据上完成的,但在保存到文件或准备图像内存块以在不同的库中使用时,跟踪颜色空间转换非常重要。 请记住,cv::imwrite需要bgr数据,而不是任何其他格式。
  • 精度、速度和资源(CPU、内存)的权衡和优化计算机视觉中的大多数问题都需要在计算和资源效率之间进行权衡。 有些算法很快,因为它们在内存中缓存关键数据,查找效率很快;其他算法可能很快,因为它们对输入或输出进行了粗略的近似,这会降低准确性。 在大多数情况下,一种吸引人的特质是以牺牲另一种特质为代价的。 不注意这些权衡,或者过于关注它们,可能会成为一个问题。工程师的一个常见陷阱是围绕优化问题。 存在欠优化或过度优化过早优化、不必要的优化等等。 在寻求优化算法时,有一种倾向是平等对待所有优化,而实际上通常只有一个罪魁祸首(代码行或方法)导致效率最低。处理算法权衡或优化主要是研究和开发时间的问题,而不是结果的问题。 工程师可能在优化上花费太多或不够的时间,或者在错误的时间进行优化。

    • 解决方案:在使用算法之前或同时了解算法。 如果您选择一种算法,请通过测试或至少查看 OpenCV 文档页面来确保您了解其复杂性(运行时和资源)。 例如,当匹配图像特征时,应该知道暴力匹配器BFMatcher通常比基于 Flann 的近似匹配器FlannBasedMatcher慢几个数量级,特别是在预加载和缓存特征是可能的情况下。

简略的 / 概括的 / 简易判罪的 / 简易的

经过 15 年的酝酿,OpenCV 正在成为一个成熟的计算机视觉库。 在此期间,它见证了许多革命的发生,无论是在计算机视觉世界还是在 OpenCV 社区。

在本章中,我们通过一个实用的视角回顾了 OpenCV 的过去,了解如何更好地使用它。 我们将重点放在一个特别好的实践上,即检查历史 OpenCV 代码以找到算法的起源,以便做出更好的选择。 为了应对丰富的功能和特性,我们还针对使用 OpenCV 开发计算机视觉应用中的一些常见缺陷提出了解决方案。

进一步阅读

有关详细信息,请参阅以下链接:

您可能感兴趣的与本文相关的镜像

PyTorch 2.7

PyTorch 2.7

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值