Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Package conflict between opencv-python and opencv-python-headless #467

Closed
Erotemic opened this issue Apr 22, 2021 · 2 comments
Closed

Package conflict between opencv-python and opencv-python-headless #467

Erotemic opened this issue Apr 22, 2021 · 2 comments

Comments

@Erotemic
Copy link

Preface: This is somewhat of a weird issue because I can't manage reproduce it with a MWE (perhaps someone here will be able to shed light on why).

I have a library: kwimage that depends on the cv2 module, and there are two main ways of obtaining this with pip. Either opencv-python or opencv-python-headless. Currently the install_requires simply specified "opencv-python".

The issue is that opencv-python contains libraries that can conflict with pyqt5, which I often see when I'm using matplotlib. Using opencv-python-headless works around this issue.

The problem is that pip has gotten too smart for its own good. If I uninstall opencv-python and install opencv-python-headless, it throws some requirements not satisfied error, but I can't figure out how to reproduce that reliably (in that I can't set up another package where it lists some package as a dependency and then remove it to reproduce the error). However, when it does happen it looks like this:

ERROR ex = DistributionNotFound(Requirement.parse('opencv-python'), {'kwimage'})
Traceback (most recent call last):
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/bin/kwcoco", line 33, in <module>
    sys.exit(load_entry_point('kwcoco', 'console_scripts', 'kwcoco')())
  File "/home/joncrall/code/kwcoco/kwcoco/cli/__main__.py", line 112, in main
    ret = main(cmdline=False, **kw)
  File "/home/joncrall/code/kwcoco/kwcoco/cli/coco_validate.py", line 81, in main
    result = dset.validate(**config_)
  File "/home/joncrall/code/kwcoco/kwcoco/coco_dataset.py", line 2670, in validate
    from kwcoco.coco_schema import COCO_SCHEMA
  File "/home/joncrall/code/kwcoco/kwcoco/coco_schema.py", line 42, in <module>
    from kwcoco.util.jsonschema_elements import SchemaElements
  File "/home/joncrall/code/kwcoco/kwcoco/util/__init__.py", line 6, in <module>
    from kwcoco.util import util_sklearn
  File "/home/joncrall/code/kwcoco/kwcoco/util/util_sklearn.py", line 6, in <module>
    from sklearn.utils.validation import check_array
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/sklearn/__init__.py", line 82, in <module>
    from .base import clone
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/sklearn/base.py", line 17, in <module>
    from .utils import _IS_32BIT
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/sklearn/utils/__init__.py", line 23, in <module>
    from .class_weight import compute_class_weight, compute_sample_weight
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/sklearn/utils/class_weight.py", line 7, in <module>
    from .validation import _deprecate_positional_args
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/sklearn/utils/validation.py", line 26, in <module>
    from .fixes import _object_dtype_isnan, parse_version
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/sklearn/utils/fixes.py", line 28, in <module>
    from pkg_resources import parse_version  # type: ignore
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/pkg_resources/__init__.py", line 3262, in <module>
    def _initialize_master_working_set():
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/pkg_resources/__init__.py", line 3245, in _call_aside
    f(*args, **kwargs)
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/pkg_resources/__init__.py", line 3274, in _initialize_master_working_set
    working_set = WorkingSet._build_master()
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/pkg_resources/__init__.py", line 584, in _build_master
    ws.require(__requires__)
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/pkg_resources/__init__.py", line 901, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/home/joncrall/.pyenv/versions/3.8.5/envs/py385/lib/python3.8/site-packages/pkg_resources/__init__.py", line 787, in resolve
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'opencv-python' distribution was not found and is required by kwimage

I'm currently going to work around this by making both opencv-python and opencv-python-headless optional installs in the install_extras section with different tags. This will solve 80% of the issue, but I think a modification to how these opencv wheels are produced might 100% solve the issue.

What I'm wondering is if it is possible for there to be an opencv-python-base package, which is effectively the headless code and then a plugin package opencv-python-graphics could be installed on top of that to only add the qt-libs? Thus the opencv-python package could maintain backwards compatibility by simply being a meta package that requires opencv-python-base and opencv-python-graphics.

I'm not sure if this is easy / possible to do, because perhaps the "non-headless" library has some compile time difference that can't be augmented at runtime.

If anyone can shed more light on the pkg_resources issue or comment on the feasibility of the "graphics-libs-as-a-plugin-module" idea, that would be appreciated.

@skvark
Copy link
Member

skvark commented Apr 22, 2021

It's not possible to separate the Qt libraries into another package because OpenCV is compiled against them. They must be present or the dynamic linking will fail during runtime.

It would be of course great to have that kind of extra packages (or just a single package, and the user could then install the extra dependencies via some other package manager), but it's not simply possible with the current OpenCV C++ side architecture. It would require that the whole C++ side is refactored to load 3rd party dependencies during runtime instead of making them hard dependencies during compile time. For example, on the Windows side, FFmpeg is loaded during runtime like this, and not having it next to the binary is not an issue.

The reason why PyQt5 conflicts with the package is that we ship with a different version of the Qt 5 platform plugin than PyQt5 and they are not ABI compatible. This is the line that breaks PyQt5: https://github.com/opencv/opencv-python/blob/master/cv2/__init__.py#L23

I think this is one of the cleanest solutions that could be used to circumvent the issue: #386 (comment) It requires some testing to confirm that it works.

Another option is to use different backend with matplotlib: matplotlib.use('tkagg')

The error you are seeing is coming from scikit-learn which uses setuptools to do something. I don't have time to dig deeper, but this is the line https://github.com/scikit-learn/scikit-learn/blob/0.24.X/sklearn/utils/fixes.py#L28 which imports parse_version and subsequently causes some side effect inside pkg_resources/__init__.py: https://github.com/pypa/setuptools/blob/main/pkg_resources/__init__.py This is not really related to pip but to some runtime checks done by scikit-learn.

@Erotemic
Copy link
Author

The unsatisfying but ultimately stable solution I've gone with is to hack my setup file with extra requirements:

        install_requires=parse_requirements('requirements/runtime.txt'),
        extras_require={
            'all': parse_requirements('requirements.txt'),
            'tests': parse_requirements('requirements/tests.txt'),
            'build': parse_requirements('requirements/build.txt'),
            # Really annoying that this is the best we can do
            # The user *must* choose either headless or graphics
            # to get a complete working install.
            'headless': parse_requirements('requirements/headless.txt'),
            'graphics': parse_requirements('requirements/graphics.txt'),
        },

The requirements directory: https://gitlab.kitware.com/computer-vision/kwcoco/-/tree/main/requirements

In headless.txt I have:

opencv-python>=4.5.4.58  ;                            python_version >= '3.10'    # Python 3.10+
opencv-python>=3.4.15.55  ; python_version < '3.10' and python_version >= '3.9'    # Python 3.9
opencv-python>=3.4.15.55  ; python_version < '3.9' and python_version >= '3.8'    # Python 3.8
opencv-python>=3.4.15.55  ; python_version < '3.8' and python_version >= '3.7'    # Python 3.7
opencv-python>=3.4.13.47  ; python_version < '3.7' and python_version >= '3.6'    # Python 3.6
opencv-python>=3.1.0.2   ; python_version < '3.6' and python_version >= '3.5'    # Python 3.5
opencv-python>=3.1.0.5   ; python_version < '3.5' and python_version >= '3.4'    # Python 3.4
opencv-python>=3.1.0.0   ; python_version < '3.4' and python_version >= '2.7'    # Python 2.7

and in graphics.txt I have:

# python ~/local/tools/supported_python_versions_pip.py opencv-python
opencv-python>=4.5.4.58  ;                            python_version >= '3.10'    # Python 3.10+
opencv-python>=3.4.15.55  ; python_version < '3.10' and python_version >= '3.9'    # Python 3.9
opencv-python>=3.4.15.55  ; python_version < '3.9' and python_version >= '3.8'    # Python 3.8
opencv-python>=3.4.15.55  ; python_version < '3.8' and python_version >= '3.7'    # Python 3.7
opencv-python>=3.4.13.47  ; python_version < '3.7' and python_version >= '3.6'    # Python 3.6
opencv-python>=3.1.0.2   ; python_version < '3.6' and python_version >= '3.5'    # Python 3.5
opencv-python>=3.1.0.5   ; python_version < '3.5' and python_version >= '3.4'    # Python 3.4
opencv-python>=3.1.0.0   ; python_version < '3.4' and python_version >= '2.7'    # Python 2.7

Then I have a check at import time in __init__.py: https://gitlab.kitware.com/computer-vision/kwimage/-/blob/main/kwimage/__init__.py#L15

import ubelt as ub
try:
    import cv2  # NOQA
except ImportError as ex:
    msg = ub.paragraph(
        '''
        The kwimage module failed to import the cv2 module.  This may be due to
        an issue https://github.com/opencv/opencv-python/issues/467 which
        prevents us from marking cv2 as package dependency.
        To work around this we require that the user install this package with
        one of the following extras tags:
        `pip install kwimage[graphics]` xor
        `pip install kwimage[headless]`.

        Alternatively, the user can directly install the cv2 package as a post
        processing step via:
        `pip install opencv-python-headless` or
        `pip install opencv-python`.

        We appologize for this issue and hope this documentation is sufficient.

        orig_ex={!r}
        ''').format(ex)
    raise ImportError(msg)

So the user has to choose [graphics] or [headless] otherwise they will get an error, but the error does try to explain what's going on.

And FWIW, I have a bash script that "ensures" the system has the headless version of opencv installed:

fix_opencv_conflicts(){
    __doc__="
    Check to see if the wrong opencv is installed, and perform steps to clean
    up the incorrect libraries and install the desired (headless) ones.
    "
    # Fix opencv issues
    pip freeze | grep "opencv-python=="
    HAS_OPENCV_RETCODE="$?"
    pip freeze | grep "opencv-python-headless=="
    HAS_OPENCV_HEADLESS_RETCODE="$?"

    # VAR == 0 means we have it
    if [[ "$HAS_OPENCV_HEADLESS_RETCODE" == "0" ]]; then
        if [[ "$HAS_OPENCV_RETCODE" == "0" ]]; then
            pip uninstall opencv-python opencv-python-headless -y
            pip install opencv-python-headless
        fi
    else
        if [[ "$HAS_OPENCV_RETCODE" == "0" ]]; then
            pip uninstall opencv-python -y
        fi
        pip install opencv-python-headless
    fi
}

Hope this helps someone. And if anyone has an improvement to this method, let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants