什么是OpenCV?

如果您还不熟悉OpenCV,请查看我们之前关于OpenCV 4.0.0的博客文章。考虑到OpenCV是一个拥有大量功能且适用于各种用例的庞大库,它是一个很好的例子,可以用来演示一些典型的包管理挑战(以及一些更具体的挑战)。

OpenCV的Conan包

最近,我们终于将OpenCV的配方接受到了conan-center。我们支持所有主要版本,因此在Bintray上提供了以下版本:

使用Conan进行安装应该非常简单,例如,您可以使用以下conanfile.txt来使用OpenCV 4.0.0。

[requires]
opencv/4.0.1@conan/stable

[generators]
cmake

像往常一样,预构建的包适用于主要平台(Windows/Linux/MacOS)和编译器(Visual Studio/GCC/Clang)。

构建OpenCV

OpenCV使用CMake,因此我们的配方使用了CMake构建助手。构建基于CMake的项目的流程对于许多配方来说都是典型的,OpenCV也不例外。

第一步是配置CMake

    def _configure_cmake(self):
        cmake = CMake(self)
        # configure various OpenCV options via cmake.definitions
        cmake.configure(build_folder=self._build_subfolder)
        return cmake

这里真的没有什么特别之处,除了有很多选项需要管理,这就是代码占用这么多行的原因。cmake.configure(…) 检测编译器及其功能,然后生成特定于平台的构建文件。

在这里,我们还禁用了我们想要避免的一些功能

        cmake.definitions['BUILD_EXAMPLES'] = False
        cmake.definitions['BUILD_DOCS'] = False
        cmake.definitions['BUILD_TESTS'] = False
        cmake.definitions['BUILD_PERF_TEST'] = False
        cmake.definitions['WITH_IPP'] = False
        cmake.definitions['BUILD_opencv_apps'] = False
        cmake.definitions['BUILD_opencv_java'] = False

cmake.definitions 是一个字典,它被转换为传递给cmake的命令行参数,例如,cmake.definitions[‘BUILD_EXAMPLES’] = False 映射到 -DBUILD_EXAMPLES=OFF

一些特定变量的解释

  • BUILD_EXAMPLES - 不要构建OpenCV示例,因为它们不需要用于使用OpenCV,但会增加构建时间和包大小。
  • BUILD_DOCS - 跳过文档,原因与示例相同,我们通常只保留链接包所需的内容,并且构建文档可能需要其他工具(例如doxygen)。
  • BUILD_TESTS - 同理,因为我们不会运行这些测试,所以跳过它们。
  • BUILD_PERF_TEST - 另一组要跳过的测试。
  • BUILD_opencv_apps - 跳过OpenCV附带的一些演示和实用程序应用程序。
  • BUILD_opencv_java - 因为我们正在构建C++的包,所以也禁用Java绑定。此外,安装它们需要JDKApache ANT等,如果没有找到,可能会失败。

一旦CMake配置完成,我们就可以构建项目了

    def build(self):
        # intentionally skipped code to patch OpenEXR here
        cmake = self._configure_cmake()
        cmake.build()

cmake.build() 根据CMake 生成器执行构建工具,它可能是MSBuildGNU MakeNinja等。这非常好,因为我们不必处理特定于平台的项目构建细节。作为反例,许多项目仍然使用不同的构建系统来编译各种平台,例如在Windows上使用Visual Studio解决方案,而在其他平台上使用makefile - 对于此类项目,配方需要有几种build方法的实现,当然,还要处理所有选项。

此外,我们配方的package方法也非常简单

    def package(self):
        cmake = self._configure_cmake()
        cmake.install()
        cmake.patch_config_paths()

它没有典型的复制特定于平台的文件(如.dll、.so、.dylib等)的代码。相反,它使用了CMake install功能。CMake可以生成一个名为INSTALL的特殊目标,它会复制项目的头文件、库、CMake 配置文件pkg-config文件,以及其他数据文件,例如OpenCV中的Haar Cascades。因此,如果项目本身知道要分发哪些文件以及如何正确布局它们,那么在conanfile中复制此逻辑就没有多大意义,对吧?此外,CMake.install方法会自动将CMAKE_INSTALL_PREFIX指向包文件夹

但是什么是cmake.patch_config_paths(),为什么我们需要它?好吧,CMake生成的配置文件可能包含绝对路径,这是我们想要避免的,因为这样的路径特定于构建配方的机器,并且使用者通常不会在相同的路径中安装依赖项。例如,在Windows上,Conan目录通常位于USERPROFILE目录中,其中包含用户名(例如AppVeyor)。考虑到这一点,使用生成的CMake配置文件可能会导致无法构建项目,因此Conan中有一个解决此问题的变通方法。

依赖项

OpenCV是一个非常复杂的库,并且有很多不同的依赖项。当前的Conan配方有以下依赖项:

dependencies of OpenCV 4.0.0 package

图表是由conan info命令生成的

$ conan info opencv/4.0.0@conan/stable --graph opencv

如您所见,它目前主要依赖于图像库,例如libjpeglibtifflibpnglibwepbjasperOpenEXR

所有这些库也作为Conan包在conan-center中可用。感谢bincrafters打包了所有这些库。

这些库主要由OpenCV imgcodecs使用,以支持读取和写入各种图像格式。

所有提到的库都可以使用选项启用或禁用(它们目前默认启用)。例如,要禁用OpenEXR支持,请使用以下方法:

[requires]
opencv/4.0.1@conan/stable

[options]
opencv:openexr=False

[generators]
cmake

声明依赖项

为了声明对其他第三方库的动态依赖,OpenCV 配方使用requirements方法

    def requirements(self):
        self.requires.add('zlib/1.2.11@conan/stable')
        if self.options.jpeg:
            self.requires.add('libjpeg/9c@bincrafters/stable')
        if self.options.tiff:
            self.requires.add('libtiff/4.0.9@bincrafters/stable')
        if self.options.webp:
            self.requires.add('libwebp/1.0.0@bincrafters/stable')
        if self.options.png:
            self.requires.add('libpng/1.6.34@bincrafters/stable')
        if self.options.jasper:
            self.requires.add('jasper/2.0.14@conan/stable')
        if self.options.openexr:
            self.requires.add('openexr/2.3.0@conan/stable')

上面的代码根据配方声明的选项添加条件依赖项

    options = {"shared": [True, False],
               "fPIC": [True, False],
               "contrib": [True, False],
               "jpeg": [True, False],
               "tiff": [True, False],
               "webp": [True, False],
               "png": [True, False],
               "jasper": [True, False],
               "openexr": [True, False],
               "gtk": [None, 2, 3]}
    default_options = {"shared": False,
                       "fPIC": True,
                       "contrib": False,
                       "jpeg": True,
                       "tiff": True,
                       "webp": True,
                       "png": True,
                       "jasper": True,
                       "openexr": True,
                       "gtk": 3}

提到的技术在文章精通Conan:条件设置、选项和需求中进行了说明。

由于我们现在使用的是来自Conan的第三方库,因此保留OpenCV源代码的3rdparty目录没有意义,因此我们在source方法中将其删除

    shutil.rmtree(os.path.join(self._source_subfolder, '3rdparty'))

为什么这很重要?有几个好处:

  • 使用者可以更好地控制依赖项,例如,他们可以通过简单地编辑他们的conanfile.txt轻松升级或降级OpenCV的第三方依赖项,如libpng。
  • 它可以节省构建时间,因为如果更改某些OpenCV选项,则不需要重新构建这些依赖项。
  • 它减少了包的大小。
  • 它有助于避免链接或运行时错误,因为如果两个库都包含libpng源代码(例如OpenCV和wxWidgets),并且您将两者都链接到您的项目中,您可能会遇到非常难以调试的问题。

最后,这些选项被传递给构建系统(OpenCV的情况是CMake)

        cmake.definitions['WITH_JPEG'] = self.options.jpeg
        cmake.definitions['WITH_TIFF'] = self.options.tiff
        cmake.definitions['WITH_WEBP'] = self.options.webp
        cmake.definitions['WITH_PNG'] = self.options.png
        cmake.definitions['WITH_JASPER'] = self.options.jasper
        cmake.definitions['WITH_OPENEXR'] = self.options.openexr

我们也始终禁用第三方库的构建

        # disable builds for all 3rd-party components, use libraries from conan only
        cmake.definitions['BUILD_ZLIB'] = False
        cmake.definitions['BUILD_TIFF'] = False
        cmake.definitions['BUILD_JASPER'] = False
        cmake.definitions['BUILD_JPEG'] = False
        cmake.definitions['BUILD_PNG'] = False
        cmake.definitions['BUILD_OPENEXR'] = False
        cmake.definitions['BUILD_WEBP'] = False
        cmake.definitions['BUILD_TBB'] = False
        cmake.definitions['BUILD_IPP_IW'] = False
        cmake.definitions['BUILD_ITT'] = False
        cmake.definitions['BUILD_JPEG_TURBO_DISABLE'] = True

因为它们是从Conan包中使用的,所以没有必要在OpenCV的上下文中从源代码构建它们。

OpenEXR的修补

CMake使用所谓的查找模块来定位各种库。大多数流行库都有很多查找模块,但是,许多库仍然缺少查找模块,OpenEXR就是其中之一。

OpenCV有一个它自己的查找模块集合,其中有一个用于OpenEXR - OpenCVFindOpenEXR

但是,OpenCV的OpenEXR模块存在几个问题

  • 它将OPENEXR_ROOT变量硬编码为Windows上的C:\deploy,因此它无法在不寻常的位置(例如Conan缓存目录)找到OpenEXR。
  • 它总是优先在系统位置(例如/usr/lib)中查找库,而OPENEXR_ROOT的优先级最低。
  • 它没有考虑OpenEXR库的所有可能名称。例如,它总是查找IlmImf,而库可能名为IlmImf-2_3_s

这很不幸。但实际上,Conan配方经常需要解决构建脚本的各种限制。令人遗憾的事实是,许多库在设计时没有考虑包管理用例,而是对路径、库名称、版本和其他重要内容进行了硬编码。这使得打包人员的工作变得更加困难,但随着C++世界中包管理的普及,我们希望这种情况发生的频率越来越低。

无论如何,目前配方中有一段代码可以删除硬编码的内容

        # allow to find conan-supplied OpenEXR
        if self.options.openexr:
            find_openexr = os.path.join(self._source_subfolder, 'cmake', 'OpenCVFindOpenEXR.cmake')
            tools.replace_in_file(find_openexr,
                                  r'SET(OPENEXR_ROOT "C:/Deploy" CACHE STRING "Path to the OpenEXR \"Deploy\" folder")',
                                  '')
            tools.replace_in_file(find_openexr, r'set(OPENEXR_ROOT "")', '')
            tools.replace_in_file(find_openexr, 'SET(OPENEXR_LIBSEARCH_SUFFIXES x64/Release x64 x64/Debug)', '')
            tools.replace_in_file(find_openexr, 'SET(OPENEXR_LIBSEARCH_SUFFIXES Win32/Release Win32 Win32/Debug)',
                                  '')

我们在这里使用tools.replace_in_file删除几行CMake代码。在更复杂的情况下,可以使用tools.patch助手。

幸运的是,OpenEXR是唯一需要修改的情况,其他库(libpng、libjpeg等)使用标准的CMake查找模块,并且它们没有上述限制。

OpenCV contrib

除了内置功能外,OpenCV还提供了一组额外的模块,称为OpenCV contrib。目前,它大约有100个额外的模块!仅举几例:

默认情况下,我们的包没有启用OpenCV contrib模块。但是,您可以通过传递opencv:contrib选项轻松获得它们:

[requires]
opencv/4.0.1@conan/stable

[options]
opencv:contrib=True

[generators]
cmake

从配方的角度来看,contrib添加了额外的源代码tarball

    tools.get("https://github.com/opencv/opencv_contrib/archive/%s.zip" % self.version)
    os.rename('opencv_contrib-%s' % self.version, 'contrib')

并将切换contrib的选项传递给构建系统(CMake)

        if self.options.contrib:
            cmake.definitions['OPENCV_EXTRA_MODULES_PATH'] = os.path.join(self.build_folder, 'contrib', 'modules')

OPENCV_EXTRA_MODULES_PATH是一个CMake变量,用于指定要构建的其他OpenCV模块,在这种情况下,我们将路径传递给contrib。

系统要求

有时配方可能需要依赖于系统包管理器提供的库,例如aptyumpacman,而不是Conan提供的库。这通常是针对一些底层内容,例如VDPAUVAAPI,但在OpenCV的情况下,它可能依赖于GTK

不幸的是,系统要求非常难以维护,因此我们的建议是,如果可能,请避免使用它们。系统要求有以下限制,这使得它们难以扩展:

  • 配方必须为每个包管理器使用其自己的分支,例如yumapt将为相同的库/包使用不同的名称(gtk2-devel vs libgtk2.0-dev)。
  • 有时,即使使用相同的包管理器,不同Linux发行版的包名也不同(例如FedoraCentOS都使用yum,但pkg-config的包名不同)。
  • 即使是同一Linux发行版的次要版本,包名也可能不同!(例如Ubuntu 16.04 vs Ubuntu 12.04)。

  • 软件包架构的名称也各不相同,例如yum使用i686x86_64后缀,而apt使用i386amd64

例如,我们目前正在使用以下代码来仅仅指定GTK依赖项

    def system_requirements(self):
        if self.settings.os == 'Linux' and tools.os_info.is_linux:
            if tools.os_info.with_apt:
                installer = tools.SystemPackageTool()
                arch_suffix = ''
                if self.settings.arch == 'x86':
                    arch_suffix = ':i386'
                elif self.settings.arch == 'x86_64':
                    arch_suffix = ':amd64'
                packages = []
                if self.options.gtk == 2:
                    packages.append('libgtk2.0-dev%s' % arch_suffix)
                elif self.options.gtk == 3:
                    packages.append('libgtk-3-dev%s' % arch_suffix)
                for package in packages:
                    installer.install(package)
            elif tools.os_info.with_yum:
                installer = tools.SystemPackageTool()
                arch_suffix = ''
                if self.settings.arch == 'x86':
                    arch_suffix = '.i686'
                elif self.settings.arch == 'x86_64':
                    arch_suffix = '.x86_64'
                packages = []
                if self.options.gtk == 2:
                    packages.append('gtk2-devel%s' % arch_suffix)
                elif self.options.gtk == 3:
                    packages.append('gtk3-devel%s' % arch_suffix)
                for package in packages:
                    installer.install(package)

这看起来非常冗余,不是吗?但是,如果我们决定为更多Linux发行版或更多架构添加支持,代码量将呈指数级增长。

如您所见,Conan使用system_requirements方法来指定系统特定的需求,并且还有一个SystemPackageTool助手,它可以自动安装软件包。在幕后,它会调用特定于给定包管理器的命令,例如apt-get install -y libgtk2.0-dev:i386

包信息

有一些平台特定的系统库,必须在conanfile的package_info方法中显式指定。

  • pthread,或POSIX线程,为兼容POSIX的系统提供多线程支持。
  • libm,C数学函数。
  • libdl,用于动态链接支持。
  • Vfw32,或Video for Windows,来自Windows 95时代的古老视频播放技术,至今仍在使用。

此外,特别是对于Apple macOS,还有许多框架正在使用。为了指定框架,我们使用以下代码

            for framework in ['OpenCL',
                              'Accelerate',
                              'CoreMedia',
                              'CoreVideo',
                              'CoreGraphics',
                              'AVFoundation',
                              'QuartzCore',
                              'Cocoa']:
                self.cpp_info.exelinkflags.append('-framework %s' % framework)
            self.cpp_info.sharedlinkflags = self.cpp_info.exelinkflags

因为它们的链接方式与库不同。大多数情况下,这些框架用于Apple平台上可用的多媒体相关技术。

未来:其他选项和依赖项

如前所述,OpenCV是一个非常庞大而复杂的库,它确实有大量的选项。目前,我们的Conan包并不支持所有这些选项。您可以在他们的GitHub仓库中查看可用选项的列表。仅仅声明所有这些选项就需要近300行的CMake代码!这实际上很难一次性建模。此外,大多数选项都依赖于其他第三方库。

仅举几个例子

Google Protocol Buffers (Protobuf)

OpenCV模块DNN(深度神经网络)可以与Google Protobuf支持一起编译。

我们目前正在积极努力添加Google Protobuf的配方,并将其纳入conan-center。该库本身极具挑战性,尤其是在交叉编译的情况下。一旦被接受,我们将启用我们的OpenCV包以默认使用Protocol Buffers。

OpenCL

OpenCV还可以配置为使用OpenCL,但是,它在各个平台上的支持差异很大,例如

  • MacOS通过提供OpenCL.framework内置了OpenCL支持。
  • Linux需要安装开发包(例如,在Debian系统上安装ocl-icd-opencl-dev)。
  • Windows需要由其中一个供应商提供的SDK包(例如,来自Intel或来自nVidia)。
  • Android也需要供应商提供的SDK包(例如,来自Mali)。

因此,为了为OpenCV包提供OpenCL支持,我们需要开发一种方法来建模这种类型的依赖关系。一种可能性可能是类似于虚拟包的东西。

CUDA

与OpenCL类似,但是,只有一个供应商,很明显——构建CUDA应用程序需要nVidia CUDA Toolkit。该工具包非常庞大,除了库和头文件外,还包含CUDA编译器。我们要么要求用户在构建期间在机器上安装CUDA,要么为工具包提供一个包。

FFMPEG

通常使用OpenCV不仅用于图像处理,还用于视频处理,例如水印、绿屏替换等。为了使OpenCV能够读取或写入视频文件,OpenCV的Video I/O模块可以使用ffmpeg库。但是,FFmpeg本身可能与OpenCV一样复杂(它的configure脚本大约有400行代码只是为了声明可用的选项!),因此它的打包也具有挑战性。希望它很快就会在conan-center中可用,这样OpenCV用户就可以捕获和写入视频流。

例如,当前的配方支持各种编码库(也使用conan打包):libx264libx265libvpxlibopenh264等。我们希望列表能够显著增长,添加诸如libaom(也称为AV1)之类的现代格式。

此外,FFmpeg也可以使用CUDA和OpenCL来加速视频编码和过滤,因此它也将受益于Conan解决CUDA和OpenCL支持。

GStreamer

我们目前正在打包GStramer库。与FFMPEG和Google Protobuf类似,GStreamer本身非常庞大,并且需要先打包一些其他库,例如libffiGLib。与FFMPEG一起,GStreamer是Conan中要求打包最多的库之一,它显然也在我们的关注范围内。

经验教训和建议

由于OpenCV的打包是一项耗费大量时间的大型任务,因此我们学到了一些经验教训,想与大家分享:

  • 对可选依赖项使用动态需求
  • 如果可能,使用构建助手,它们可以自动化许多事情,并使配方代码保持简洁。
  • patch_config_paths可能需要用于CMake库。
  • 使用exelinkflags/sharedlinkflags来指定Apple框架
  • 如果可能,避免使用系统需求,而是使用Conan打包库。

结论

尽管OpenCV包在conan-center中可用,但它们在支持的选项和依赖项方面并不完整,我们正在考虑以小迭代的方式添加更多选项,以满足更多用例并支持更多功能。

但我们仍然鼓励用户尝试我们的OpenCV包,并将任何问题和功能请求报告给我们的GitHub。我们将根据反馈优先考虑添加缺失的部分。

总的来说,Conan已经足够灵活和成熟,可以处理非常复杂的库(例如OpenCV)的打包,并且conanfile可以处理所有需求、选项、修补等。此外,Conan提供了一些工具和助手,使打包人员的生活更加轻松,节省时间。

Conan还清楚地分离了conanfile中的逻辑,使读取和编写配方代码变得更加容易,并且Conan允许逐步调试配方,逐个调用其步骤:source -> build -> package -> test