写这篇文章是因为我想在实验室的服务器上跑 CUDA 加速版本的 OpenCV,但是服务器上安装的 OpenCV 是没有 CUDA 加速的,找管理员重装 OpenCV 又很麻烦,所以我最终研究出了编译安装 OpenCV,同时不需要 sudo 权限,而且可以一并安装 conda 环境中的 opencv-python 的方法。其实之前打机器人比赛的时候我们团队也经常遇到类似的问题,那时主要的困难是编译安装的 OpenCV 要么只能在全局的 python 里使用,要么只能在虚拟环境中使用,在另一侧import cv2会报错,如果还有其它库(例如 ROS)基于 OpenCV 和 Python 的话会把环境搞的非常非常乱。

废话不多说直接上方法,其实就是在编译的时候直接指定安装路径和 Python 的位置为虚拟环境,道理很好想明白只是不一定知道怎么操作,以下是我cmake时提供的参数:

cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=$CONDA_PREFIX \
-D INSTALL_PYTHON_EXAMPLES=OFF \
-D INSTALL_C_EXAMPLES=OFF \
-D OPENCV_ENABLE_NONFREE=ON \
-D WITH_CUDA=ON \
-D WITH_CUDNN=ON \
-D OPENCV_DNN_CUDA=ON \
-D ENABLE_FAST_MATH=ON \
-D CUDA_FAST_MATH=ON \
-D CUDA_ARCH_BIN=8.6 \
-D WITH_CUBLAS=ON \
-D OPENCV_EXTRA_MODULES_PATH=../../contrib/modules \
-D HAVE_opencv_python3=ON \
-D PYTHON3_EXECUTABLE=$(which python) \
-D PYTHON3_INCLUDE_DIR=$(python -c "from distutils.sysconfig import get_python_inc; print(get_python_inc())") \
-D PYTHON3_NUMPY_INCLUDE_DIRS=$(python -c "import numpy; print(numpy.get_include())") \
-D PYTHON3_PACKAGES_PATH=$(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") \
-D BUILD_EXAMPLES=OFF \
-D WITH_CUDEV=ON \
-D WITH_NVCUVID=ON \
-D WITH_GTK=ON \
-D WITH_QT=ON \
-D WITH_OPENGL=ON \
-D WITH_VULKAN=ON \
-D CMAKE_C_COMPILER=/usr/bin/gcc-14 \
-D CMAKE_CXX_COMPILER=/usr/bin/g++-14 ..

其中大部分参数都不是重点,主要是CMAKE_INSTALL_PREFIXPYTHON3_EXECUTABLEPYTHON3_INCLUDE_DIRPYTHON3_PACKAGES_PATH这四个,其中CMAKE_INSTALL_PREFIXmake install时二进制文件被复制到的位置,CONDA_PREFIX是当前虚拟环境的根路径,另外三个参数有 CMake 基础都明白是什么意思,主要是参数的值,在对应的虚拟环境下照抄即可。

需要注意安装步骤应该直接使用make install而不是sudo make install,因为~/miniconda3(或者~/anaconda3)对于当前用户而言当然是有写权限的。

通过这种方法安装以后,import cv2时可能还会出现以下问题:

>>> import cv2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/***/miniconda3/envs/***/lib/python3.11/site-packages/cv2/__init__.py", line 181, in <module>
    bootstrap()
  File "/home/***/miniconda3/envs/***/lib/python3.11/site-packages/cv2/__init__.py", line 153, in bootstrap
    native_module = importlib.import_module("cv2")
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/***/miniconda3/envs/***/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ImportError: /home/***/miniconda3/envs/***/bin/../lib/libstdc++.so.6: version `CXXABI_1.3.15' not found (required by /home/***/miniconda3/envs/***/lib/python3.11/site-packages/cv2/python-3.11/cv2.cpython-311-x86_64-linux-gnu.so)

请注意这里出错的动态链接库不一定是我这里的libstdc++.so.6,但出错的原因应该都是一样的。这因为编译流程中的符号决议(Symbol Resolution)过程中,编译器找到了位于全局的动态链接库/usr/lib64/libstdc++.so.6(我是 Fedora 发型版,如果你是 Ubuntu,那应该是位于/usr/lib/x86_64-linux-gnu/libstdc++.so.6)并采用了较新版本的符号CXXABI_1.3.15,而在运行阶段,加载的动态链接库是虚拟环境中的旧版本动态链接库,因没有找到对应的符号而发生了运行时链接失败。

最根本解决这个问题的方法是在cmake时提供CMAKE_PREFIX_PATH参数指向虚拟环境的根目录,这么做会让 CMake 从虚拟环境中搜索编译所需的全部依赖,这种解决方法最彻底但也有非常明显的弊端——在虚拟环境中安装库的全部依赖是一件既消耗精力又占用空间的行为,所以这里我提供另一种解决方法。

答案就是在启动 Python 前强制指定具体加载哪一个动态链接库,方法就是使用LD_PRELOAD环境变量:

export LD_PRELOAD=/lib64/libstdc++.so.6
python -c "import cv2"

可以发现已经不报错了。

但是每次启动 Python 前都要手动指定LD_PRELOAD会很麻烦,直接将其写在.bashrc里又显得破坏了虚拟环境的隔离性,很不优雅,有没有办法让 Conda 来管理环境变量的值?其实也是有的,虚拟环境在被 activate 和 deactivate 时会分别执行$CONDA_PREFIX/etc/conda/activate.d$CONDA_PREFIX/etc/conda/deactivate.d下的脚本,可以用它们来配置环境变量的隔离。

我的$CONDA_PREFIX/etc/conda/activate.d/env_vars.sh

#!/bin/bash

export LD_PRELOAD_OLD="$LD_PRELOAD"
export LD_PRELOAD="/lib64/libstdc++.so.6:$LD_PRELOAD"

我的$CONDA_PREFIX/etc/conda/deactivate.d/env_vars.sh

#!/bin/bash

export LD_PRELOAD="$LD_PRELOAD_OLD"
unset LD_PRELOAD_OLD

这样就可以让 Conda 来帮我们管理环境变量了。

文章作者: 卡比三卖萌KirCute
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 卡比三卖萌KirCute的博客
开发记录
喜欢就支持一下吧