直到最近,OpenCV Python 包才适用于 Windows、Linux(x86_64 和 ARM)以及 macOS(以前称为 OSX)的 x86_64,世界一片和谐。然而,在 2020 年 11 月,Apple 推出了其 M1 处理器,并随之推出了一系列基于该处理器的全新硬件,这改变了游戏规则 - macOS 现在不仅需要 x86_64 包,还需要 arm64 包!
与此同时,opencv-python 发生了另一个变化 - 我们已将构建流程切换到 GitHub Actions,因为我们使用过的其他持续集成 (CI) 平台变得过于限制。但是,Actions 没有提供 macOS M1 构建器。这给我们计划带来了另一个难题。
为了帮助缓解这两个问题,我们购买了一台 M1 Mac,以便我们能够以正确的方式在此新硬件上进行构建。这需要一些额外的努力,但我们最终做到了,以下就是操作步骤!
关于作者
Grigory Serebryakov 是 OpenCV AI 首席开发官。
如何为 OpenCV 开发设置 macOS
我从 官方教程 开始,该教程介绍了如何为 macOS 构建当前版本的 OpenCV。但是,对于我们的持续集成机器,我们需要执行一些额外的步骤。由于我们希望支持多个 Python 版本,未来可能会有更多机器,因此我们需要自动执行环境设置。
在同一台机器上管理 Python 解释器时,我选择使用 pyenv(是的,我们知道还有其他选择)。由于我们讨论的是自动化,我们需要能够从命令行安装软件,以有效地管理依赖项。免费的 Homebrew 是 macOS 的一个很棒的第三方包管理器,我们将用它来实现此目的。最重要的是,自动化需要不同工具之间的一些粘合剂来配置它们,提供必要的设置,并报告在设置新机器时遇到的问题。 Ansible 是该领域使用最广泛的工具之一,并且具有良好的记录,因此我们将使用它。
我们不要忘记我们仍然可能需要一些 GUI 应用程序 - 是的,这台机器是用于 CI,但想象一下,在某个时刻你可能想调试一些功能。也就是说,拥有编译器还不够,我们需要一个 IDE。使用这个 Ansible 集合,它允许你从 Apple 商店安装任何 GUI 应用程序,我们将设置 XCode,这是 Apple 推荐的 IDE。使用上述工具,我们拥有了设置需求所需的一切,可以继续构建。
使用 Python 3.9 构建 OpenCV
从晚上开始,我打开笔记本电脑,通过 ssh 连接到 M1 机器,检查所有工具是否已准备好,并克隆了 OpenCV 存储库。之后,我开始构建 - 最初不是 Python 包,而是 OpenCV 本身,以及 Python 绑定。
我从 pyenv 中安装了 python 3.9 - 像这样简单
$ pyenv install 3.9.5
现在我们准备配置构建。首先,我们将 Python 的活动版本设置为 3.9.5,确保 pip 已更新,然后安装构建所需的 python 包
$ pyenv local 3.9.5
$ python3 -m pip install --upgrade pip
$ python3 -m pip install numpy
现在,我们应该向 cmake 提供这 3 个选项以构建 python3 模块
- PYTHON3_EXECUTABLE
- PYTHON3_INCLUDE_DIR
- PYTHON3_NUMPY_INCLUDE_DIRS
这会带来一个问题:假设我们有一个非系统 Python,我们可以在哪里找到这些路径?(不要忘记,如果你在本地执行此操作,路径可能与下面显示的路径不同。)
pyenv 本身可以告诉我们 Python 的二进制文件在哪里,这是一个
$ pyenv which python3
/Users/xperience/.pyenv/versions/3.9.5/bin/python3
… 以及我们的 Python 安装提供了一个名为 python-config 的实用程序,它知道有关库和包含目录的所有信息
$ python3-config --includes
-I/Users/xperience/.pyenv/versions/3.9.5/include/python3.9
最后一个是我们的 numpy 包含目录,我们可以这样获取
$ python3 -c "import numpy; print(numpy.get_include())"
/Users/xperience/.local/lib/python3.9/site-packages/numpy/core/include
瞧,现在让我们创建一个构建目录并运行 cmake
$ mkdir opencv_build
$ cd opencv_build
$ cmake -DPYTHON3_EXECUTABLE=$(pyenv which python3) \
-DPYTHON3_INCLUDE_DIR=~/.pyenv/versions/3.9.5/include/python3.9 \
-DPYTHON3_NUMPY_INCLUDE_DIRS=~/.local/lib/python3.8/site-packages/numpy/core/include \
../opencv
检查 cmake 的输出以确保它列出了 python3 模块 - 它应该在要构建的模块列表中。如果一切正常,我们就可以构建了
$ cmake --build . -j8
为其他 Python 版本构建 OpenCV
我在午夜时分得到了上述结果,并且带着完成任务的良好感觉去睡觉了。第二天,我让我的同事检查了相同步骤,但用于其他 Python 版本 - 3.7.10 和 3.8.10,因为我计划在接下来的几天休假。
想象一下我的惊讶,当我回到工作岗位并收到以下消息时
> 一切都很糟糕。Python 3.8 需要打补丁(使用官方补丁,但仍然如此)。Python 3.7 在安装后无法使用 _ctypes 模块
这不是你期望在使用 Python 3.9 进行干净构建后得到的结果,对吧?我们必须亲自动手来了解发生了什么。
Python 3.8 和 M1 上的 macOS
这个版本似乎比 3.7 更容易修复 - 我们能够获得工作 Python,但需要一个补丁。为什么我们不喜欢这种解决方案?此补丁链接对于每个 Python 版本都是唯一的,因此维护这些链接的列表可能会变得有点难以管理。然而,在阅读了 GitHub 和 Stack Overflow 上的讨论后,我开始思考我们是否可以避免打补丁。我在 pyenv Github 上的一个问题中找到了最终的解决方案。Apple 已经考虑到了需要运行 x86_64 应用程序的新 ARM 机器用户,并提供了一个兼容性层来促进这一点。不幸的是,对于我们来说,构建机制认为我们正在 x86_64 机器上构建,而不是 ARM,这是由于这个层!解决方法是将环境正确设置为我们的 homebrew 路径
export LDFLAGS="-L/opt/homebrew/lib"
export CPPFLAGS="-I/opt/homebrew/include"
就这样 - 在进行此简单的调整后,我们可以在没有任何补丁的情况下构建 Python 3.8!
Python3.7 和 M1 上的 macOS
修复了 3.8 后,我可能有点过于自信了。Python 3.7 构建问题很奇怪 - 我们能够构建它,但 python 出错,指出没有提供模块 `_ctypes`。这个模块对我们来说至关重要,就像对 Python 的任何二进制库一样(因为数据交换依赖于 C 兼容接口和 C 数据类型)。换句话说,这意味着没有 ctypes,我们就无法使用 numpy 或 OpenCV。首先,我尝试导出解决 3.8 构建的相同变量,结果… 什么都没有。查看构建日志,我发现它尝试构建 _ctypes。但是,代码显示(并出现错误)macOS/OSX 无法作为操作系统和 ARM 作为平台同时存在(哈哈,现在不再如此了!)。
一些搜索告诉我,如果我有一个特定于系统的 libffi(一个描述外部函数接口并允许与 C 兼容接口进行数据交换的库 - 正是我们需要的!),我可以避免构建这部分代码。我用 Homebrew 检查了一下 - 是的,libffi 存在,但 python 无法识别它。
解决方案来自 python3.9 构建日志 - 它指定了以下定义
`-DMACOSX -DUSING_APPLE_OS_LIBFFI=1`.
对 Python 构建配置管理和 pyenv 的 python-build 模块进行一些研究后,我发现有一个标志 `–with-system-ffi` 可以通过 pyenv 传递给 python 构建工具,例如:
CONFIGURE_OPTS='--with-system-ffi' pyenv install 3.7.10
最终检查:我构建了 Python,启动了解释器,并尝试导入 `_ctypes`。你可以猜到发生了什么。是的,我收到了一个错误。但这个错误是一个新的错误
ctypes/__init__.py
CFUNCTYPE(c_int)(lambda: None)
MemoryError
导入时出现内存错误?这是新的。是时候再次打开搜索引擎了!四处搜索,我发现了一些东西:来自 python2.6 的一个旧问题。尽管是较旧的版本,但这就是我们遇到的错误的根源,更重要的是,2.6 的解决方案仍然适用于 3.7:只需删除导致内存错误的代码即可。以下是该代码
def _reset_cache(): _pointer_type_cache.clear() _c_functype_cache.clear() if _os.name == "nt": _win_functype_cache.clear() # _SimpleCData.c_wchar_p_from_param POINTER(c_wchar).from_param = c_wchar_p.from_param # _SimpleCData.c_char_p_from_param POINTER(c_char).from_param = c_char_p.from_param _pointer_type_cache[None] = c_void_p # XXX for whatever reasons, creating the first instance of a callback # function is needed for the unittests on Win64 to succeed. This MAY # be a compiler bug, since the problem occurs only when _ctypes is # compiled with the MS SDK compiler. Or an uninitialized variable? CFUNCTYPE(c_int)(lambda: None)
最后一行是这里发出内存错误的部分,正如它附近的注释所说,它的作者不知道为什么需要这段代码,但它是专门针对 Windows 的,是为了让测试顺利进行。当然,我们在 macOS 上。
所以… 让我们删除最后一行。坦率地说,我还没有胆量只依赖 Stack Overflow 的评论,因此我检查了 python3.8 的源代码。猜猜看?那行代码不再存在 - 它已经被删除了!你可能会好奇 - 他们为什么从 3.8 中删除了这段代码,却没有从 3.7 中删除?答案是 3.7 已经过了其“生命周期终结”,因此不再接受任何增强功能,只有针对安全漏洞的错误修复。
呼!有了所有这些,我们终于安装了 python3.7,并且能够用它构建 opencv。
这篇文章结束了吗?不,还没有!
OpenCV-Python 包:您的操作系统太新了
在完成上述所有步骤后,最后一个步骤 - 使用 opencv-python 准备二进制轮子看起来微不足道。但是,我再次遇到了一些问题。
我们已经克隆了 opencv-python 仓库,所有 Python 版本都已经到位,所以我们开始构建。我们获得了包,一切看起来都如预期一样,所以只剩下最后一步:检查构建包的功能。在这里我们发现了下一个问题:我们无法使用 pip 成功安装任何包!然而,我的同事 Andrey 找到了解决方案。
问题是我们 macOS 的版本太新了。我们有 OSX 11.1,我们的包名称类似于 `opencv_python-4.5.2-cp39-cp39-macosx_11_1_arm64.whl`。但是,目前 pip 只知道 macOS 11.0 - 你可以使用以下命令检查它
$ python3 -m pip debug -v | grep -A 10 'Compatible tags'
Compatible tags: 327
cp39-cp39-macosx_11_0_arm64
cp39-cp39-macosx_11_0_universal2
cp39-cp39-macosx_10_16_universal2
cp39-cp39-macosx_10_15_universal2
cp39-cp39-macosx_10_14_universal2
cp39-cp39-macosx_10_13_universal2
cp39-cp39-macosx_10_12_universal2
cp39-cp39-macosx_10_11_universal2
cp39-cp39-macosx_10_10_universal2
cp39-cp39-macosx_10_9_universal2
以下设置解决了我们的 pip 安装问题
export MACOSX_DEPLOYMENT_TARGET=11.0
python3 -m pip wheel
将 macOS 构建带回 GitHub 世界
在之前的步骤中,我们在 M1 架构的 macOS 上为 opencv-python 构建了包。但是,请记住,我们的最终目标是将这些构建与 GitHub 上的发布过程无缝集成。为此,我们需要一个在我们的 M1 主机上运行的 GitHub Actions 运行器。在我写这些文字的时候,Github Actions 运行器只有 macOS 的 x86_64 版本。这看起来不是什么大问题 - 请记住,Apple 为 M1 上的 x86_64 应用程序提供了一层兼容性,因此运行器即使其目标架构不同也能正常工作。不幸的是,当我们在 Github Actions 场景中构建 opencv-python 时,构建机制的一部分会将平台识别为 x86_64 *即使我们已经修复了环境*,所以我们得到了错误的结果。幸运的是,解决方案很简单:使用 `arch` 实用程序,我们可以“运行通用二进制文件的选定架构”
运行完此操作后,一切正常
arch -arm64 python${{ matrix.python-version }} -m pip wheel --wheel-dir=wheelhouse . --verbose
欢迎 M1 上的 macOS opencv-python
最后,经过上述所有努力,我们有一个 拉取请求 最近被合并。它为原生 macOS M1 包添加了 CI,并支持 python3.7、3.8 和 3.9 构建。
很快我们将在 PyPI 上发布这些包。非常感谢 Andrey Senyaev,没有他的帮助,这一切将不可能实现。