直到最近,OpenCV的Python包被提供给Windows、Linux(x86_64和ARM)和macOS(以前称为OSX)的x86_64,世界上一切都很正常。然而,在2020年11月,苹果公司推出了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集合,你可以从苹果商店安装任何GUI应用程序,我们将设置XCode,当然,这是苹果推荐的IDE。使用上述工具,我们就拥有了我们所需要的一切来设置我们的需求,并继续进行构建。
用Python3.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 include目录,我们这样得到它。
$ python3 -c "import numpy; print(numpy.get_include())"
/Users/xperience/.local/lib/python3.9/site-packages/numpy/core/include
Voilà,现在让我们创建一个构建目录并运行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进行干净的构建后得到的东西,对吧?我们必须亲自动手才能明白发生了什么事。
M1 上的 Python 3.8 和 macOS
这个版本似乎比3.7更容易解决--我们能够得到工作中的Python,但需要一个补丁。为什么我们不喜欢这个解决方案?这个补丁链接对每个Python版本都是唯一的,所以维护这些补丁的列表会变得有点难以管理。然而,在阅读了GitHub和Stack Overflow上的讨论后,我开始思考,我们可以避免打补丁的部分。我在pyenv Github的一个问题中找到了最终的解决方案。苹果公司已经考虑到新的ARM机器的用户需要运行x86_64应用程序,并提供了一个兼容层来促进这一点。对我们来说,不幸的是,构建机器认为我们是在x86_64机器上构建的,而不是在ARM机器上,因为这个层的存在。修复方法是将环境正确地设置为我们自制的路径。
export LDFLAGS="-L/opt/homebrew/lib"
export CPPFLAGS="-I/opt/homebrew/include"
就是这样--经过这个简单的调整,我们可以构建Python 3.8,而不需要那个补丁了!
M1 上的 Python3.7 和 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-pythonrepo,所有的python版本都已经到位了,所以我们开始构建。我们已经得到了软件包,一切似乎都符合预期,所以只有最后一步摆在我们面前:检查所建软件包的功能。在这里,我们发现了下一个问题:没有一个包我们能够用pip成功安装!但是,我的一位同事,即 "小白",在他的帮助下,我们成功地安装了这些包。不过,我的一个同事Andrey找到了解决办法。
问题是,我们的macOS版本太新了。我们的操作系统是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 runner。在我写这些文字的时候,Github Actions runner只有macOS的x86_64版本。这看起来不是什么大问题--记住,苹果为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
最后,经过以上的斗争,我们有了一个Pull Request,最近被合并了。它为原生macOS M1包增加了CI,并支持python3.7、3.8和3.9的构建。
很快,我们将在PyPI上发布这些软件包。非常感谢Andrey Senyaev,没有他的帮助,这不可能实现。