老周有AI

计算机视觉与人工智能技术分享

一键复制依赖库脚本

在Linux开发中,当我们需要将编译好的动态库部署到其他机器时,经常会遇到依赖缺失的问题。手动查找和复制所有依赖库非常繁琐,这个脚本可以帮你自动完成这项工作。

功能介绍

  • 自动识别目标动态库的所有依赖项
  • 创建专用目录存放依赖文件
  • 一键复制所有依赖到指定位置

使用方法

#!/bin/bash

# 定义源.so文件路径、目标依赖存放路径和so文件名
SO_FILE_PATH="libPID.so"
DEPENDS_DIR="depends"

# 检查并创建depends目录,如果它不存在
if [ ! -d "$DEPENDS_DIR" ]; then
    mkdir -p "$DEPENDS_DIR"
    echo "Directory $DEPENDS_DIR created."
fi

# 使用ldd获取.so文件的所有依赖,并循环处理每一行
while IFS= read -r line; do
    # 每一行格式类似 "libxxx.so => /path/to/libxxx.so (version)"
    if [[ $line == *"=>"* ]]; then
        lib_path=$(echo "$line" | awk -F '=>' '{print $2}' | awk '{print $1}')
        
        # 如果路径非空并且不是"(0x00000000)"这样的形式(表示静态链接或未找到)
        if [[ ! -z "$lib_path" && "$lib_path" != \"\(*\)\" ]]; then
            lib_name=$(basename "$lib_path")

            # 将依赖库复制到目标目录
            cp "$lib_path" "$DEPENDS_DIR/$lib_name"
            echo "Copied $lib_path to $DEPENDS_DIR/$lib_name"
        fi
    fi
done < <(ldd "$SO_FILE_PATH")

echo "Done."

参数说明

  • SO_FILE_PATH: 目标动态库文件路径(默认:libPID.so)
  • DEPENDS_DIR: 依赖库存放目录(默认:depends)

使用步骤

  1. 修改脚本中的SO_FILE_PATH指向你的动态库
  2. 赋予执行权限: chmod +x onekeycopy_depends.sh
  3. 运行脚本: ./onekeycopy_depends.sh

注意事项

  • 需要系统安装ldd命令
  • 仅支持动态链接的依赖库
  • 确保对目标动态库有读取权限

相关项目

以下是使用这些资源的项目:

Eye Laser Game

  • 项目地址 - 基于眼睛追踪的激光射击游戏

Gesture Snake Game

  • 项目地址 - 手势控制贪吃蛇游戏

AR Pi Tattoo

  • 项目地址 - 基于AR的圆周率纹身互动应用

Head Racing Game

  • 项目地址 - 头部运动控制的赛车游戏

Face Mask Application

  • 项目地址 - 实时人脸面具应用

Hand Gesture Volume Control

  • 项目地址 - 手势音量控制系统

Scan Effect Generator

  • 项目地址 - 扫描线效果生成器

Virtual Drag and Drop

  • 项目地址 - 虚拟拖放与缩放应用

联系方式

WeChat Bilibili Douyin Xiaohongshu CSDN
WeChat QR Code Bilibili QR Code Douyin QR Code Xiaohongshu QR Code CSDN QR Code

md2docx:让Markdown文档秒变精美Word文档的神器

Github代码仓库

你是否曾经为了将Markdown笔记转换成公司要求的Word格式而头疼?是否厌倦了复制粘贴后还要手动调整格式?md2docx来解救你了!

🚀 这是什么神器?

md2docx是一款强大的Markdown转Word文档工具,它能够将你喜爱的Markdown格式无缝转换为精美的DOCX文档,保留原有的格式和样式,让你的文档在任何场合都能完美展示。

作为一名程序员,我习惯用Markdown记录笔记、写文档,但公司和客户往往要求提供Word格式的文件。每次手动转换都是一场噩梦,于是我开发了这个工具,希望能帮助到和我有同样困扰的朋友们。

✨ 主要特性

  • 完整的Markdown语法支持:标题、列表、代码块、表格、引用、图片等全部支持
  • 格式精准转换:精确还原Markdown的层级结构和样式
  • 批量处理能力:一次性转换多个文件,效率翻倍
  • 简单易用:命令行工具,一行代码搞定转换
  • 本地处理:所有转换在本地完成,无需担心隐私泄露

🔍 它能转换哪些元素?

md2docx几乎支持所有常用的Markdown语法元素:

1. 文本格式化

  • 各级标题(H1-H6)
  • 粗体、斜体、删除线
  • 引用块(支持多层嵌套)

2. 结构元素

  • 有序列表和无序列表(支持多级嵌套)
  • 任务列表(TODO列表)
  • 分隔线
  • 代码块(带语法高亮)

3. 媒体和链接

  • 图片插入(本地和网络图片)
  • 超链接(内联链接和引用链接)

4. 表格

  • 基础表格
  • 对齐方式
  • 复杂表格样式

5. HTML支持

  • 基础HTML标签转换

🚀 如何使用

使用md2docx非常简单,只需一行命令:

python -m md2docx input.md output.docx

批量转换更是轻而易举:

python batch_convert_test.py --input-dir your_md_folder --output-dir your_docx_folder

💡 使用场景

  • 技术文档转换:将GitHub上的README或技术文档转为公司内部可用的Word格式
  • 博客文章导出:把你的Markdown博客文章导出为Word以便发布到其他平台
  • 学术论文写作:用Markdown高效写作,然后转换为学校要求的Word格式提交
  • 工作报告生成:使用Markdown编写工作报告,转换后直接提交给领导

🔮 未来计划

我们正在开发更多激动人心的功能:

  • 图形用户界面,让非技术用户也能轻松使用
  • 自定义样式模板,让你的文档更具个性
  • 更多高级功能支持,如数学公式、流程图等
  • 双向转换,支持Word转回Markdown

🙏 结语

md2docx诞生于实际需求,成长于社区反馈。如果你也是Markdown爱好者,却被Word格式所困扰,不妨试试这个工具。它或许不是最完美的,但一定是最实用的。

欢迎在GitHub上给我们提出建议或贡献代码,让这个工具变得更好!


工具不仅仅是为了解决问题,更是为了让工作变得更加愉悦。

—— md2docx开发团队

问题–垃圾太多,系统盘空间占用太大

最近 C 盘空间暴涨,用工具 WinDirStat-强烈推荐的工具 查看发现 WSL 子系统占用了6个多 G 的空间,遂想办法挪个位置;
【关键字】将 Windows 里的子系统挪到非系统盘 D 盘;

解决

  1. 打开 WSL 控制台
wsl -l -v # 查看当前子系统
PS D:\> wsl -l  -v
  NAME            STATE           VERSION
* Ubuntu-22.04    Stopped         2
  1. 导出
wsl --export Ubuntu-22.04 d:\ubuntu2204.tar
正在导出,这可能需要几分钟时间。
操作成功完成。
  1. 注销
PS D:\> wsl --unregister Ubuntu-22.04
正在注销。
操作成功完成。
  1. 重新导入
PS D:\> wsl --import Ubuntu-22.04 d:\Ubuntu-22.04 d:\ubuntu2204.tar --version 2
正在导入,这可能需要几分钟时间。
操作成功完成。

补充清理其他垃圾

清理 WPS 垃圾

路径为: C:\Users\xxx\AppData\Roaming\kingsoft\wps\addons\pool\win-i386

照按上面的路径一层层的打开文件夹,win-i386 文件夹里的内容都是无用的垃圾,可以统统清除掉,alt+a 全选,右击【删除】

temp 垃圾

win+R 键,在命令框中输入%temp%,打开的文件夹里的都是系统缓存文件,alt+a全选,右键删除即可。

更改 Android AVD 的路径

AVD 的默认路径仍然在 C 盘,且是以 GB 为单位在增长。

  • 新建一个环境变量

打开 Android Studio, D:\Android\Android_AVD 路径下会多出来一个 .android 文件夹,将 C 盘下对应路径的 .android 文件夹拷贝过来覆盖;

  • 修改配置

在对应的 AVD 下如: D:\Android\Android_AVD\.android\avd\Pixel_4_API_30.avd

找到配置文件 hardware-qemu.inictrl + H 找到对应的老路径修改为新放置的路径。

工具推荐

Dism++,也许是最强的实用工具, 一般一下子能干掉几个 G 的垃圾。

Everything Windows下基于名称快速定位文件和文件夹

【参考】

电脑C盘爆满,10秒快速清理C盘垃圾
更改Android Studio中AVD的默认路径

很多小伙伴在 Windows 下做深度学习开发的时候,遇到终端没有在 Linux 那么方便,那么我们现在就可以来设置一下;这样我们也可以在文件夹内部右键打开终端,也可以在 VS Code 里面新建一个虚拟环境的控制台;这里主要是针对 Anaconda 环境下的虚拟环境进行终端配置,这样就可以很方便的在终端打开虚拟环境,不用搜索和应用程序里打开 Anaconda Prompt ;

  1. 打开应用商店,安装 Windows Terminal ;
  1. 在文件夹内部可以右键打开终端了;
  1. 在打开的终端里,我们也可以配置

可以在名称里设置成 Anaconda Prompt
命令行里写上 cmd.exe /k "d:\anaconda3\Scripts\activate.bat" yolov8, 其中 "d:\anaconda3\Scripts\activate.bat"activate 批处理路径, yolov8 为虚拟环境的名称;
启动目录你可以设置为常用的项目目录;
最后保存;

这样你可以打开终端的时候,就可以直接选择 Anaconda Prompt 了;

  1. vs code 下 按住 ctrl + ` 召唤终端,
        "Anaconda Prompt": {
            "path": [
                "${env:windir}\\Sysnative\\cmd.exe",
                "${env:windir}\\System32\\cmd.exe"
            ],
            "args": ["/k", "d:\\anaconda3\\Scripts\\activate.bat" , "yolov8"],
            "icon": "terminal-cmd"        
        },

好了,这样下次就可以直接在 VS Code 里打开 Anaconda Prompt 终端了。

问题如下:
Windows10 运行 YOLOv8 出现如下错误:
Traceback (most recent call last):
File “”, line 1, in
File “D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py”, line 116, in spawn_main
exitcode = _main(fd, parent_sentinel)
^^^^^^^^^^^^^^^^^^^^^^^^^
File “D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py”, line 125, in _main
prepare(preparation_data)

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py", line 122, in spawn_main
    exitcode = _main(fd, parent_sentinel)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py", line 131, in _main
    prepare(preparation_data)
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py", line 242, in prepare
    _fixup_main_from_path(data['init_main_from_path'])
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py", line 293, in _fixup_main_from_path
    main_content = runpy.run_path(main_path,
                   ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen runpy>", line 291, in run_path
  File "<frozen runpy>", line 98, in _run_module_code
  File "<frozen runpy>", line 88, in _run_code
  File "F:\workspace\yolov8\train.py", line 8, in <module>
    results = model.train(data="fsd.yaml", epochs=200, batch=16) 
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\engine\model.py", line 377, in train
    self.trainer.train()
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\engine\trainer.py", line 192, in train
    self._do_train(world_size)
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\engine\trainer.py", line 294, in _do_train
    self._setup_train(world_size)
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\engine\trainer.py", line 259, in _setup_train
    self.train_loader = self.get_dataloader(self.trainset, batch_size=batch_size, rank=RANK, mode='train')
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\models\yolo\detect\train.py", line 40, in get_dataloader
    return build_dataloader(dataset, batch_size, workers, shuffle, rank)  # return dataloader
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\data\build.py", line 101, in build_dataloader
    return InfiniteDataLoader(dataset=dataset,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\data\build.py", line 29, in __init__
    self.iterator = super().__iter__()
                    ^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\torch\utils\data\dataloader.py", line 441, in __iter__
    return self._get_iterator()
           ^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\torch\utils\data\dataloader.py", line 388, in _get_iterator
    return _MultiProcessingDataLoaderIter(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\site-packages\torch\utils\data\dataloader.py", line 1042, in __init__
    w.start()
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\process.py", line 121, in start
    self._popen = self._Popen(self)
                  ^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\context.py", line 224, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\context.py", line 336, in _Popen
    return Popen(process_obj)
           ^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\popen_spawn_win32.py", line 45, in __init__
    prep_data = spawn.get_preparation_data(process_obj._name)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py", line 160, in get_preparation_data
    _check_not_importing_main()
  File "D:\anaconda3\envs\yolov8\Lib\multiprocessing\spawn.py", line 140, in _check_not_importing_main
    raise RuntimeError('''
RuntimeError:
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.

修改

D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\data 路径下的 build.py 文件里的函数 build_dataloader, 将 nw = min([os.cpu_count() // max(nd, 1), batch if batch > 1 else 0, workers]) # number of workers 直接修改为 nw=0

def build_dataloader(dataset, batch, workers, shuffle=True, rank=-1):
    """Return an InfiniteDataLoader or DataLoader for training or validation set."""
    batch = min(batch, len(dataset))
    nd = torch.cuda.device_count()  # number of CUDA devices
    # nw = min([os.cpu_count() // max(nd, 1), batch if batch > 1 else 0, workers])  # number of workers
    nw = 0 # update by jw 20230815
    .....

【参考】

解决RuntimeError: An attempt has been made to start a new process before…办法

python进程 - 调试报错 you are not using fork to start your child processes

Windows 10 安装 PyTorch 开发环境,以及验证 YOLOv8

环境:
OS: Windows 10
显卡: RTX 3090

安装 NVIDIA 驱动

根据显卡型号找到对应的驱动进行安装

GeForce® 驱动程序

验证

在终端中输入: nvidia-smi 查看是否正确安装

PS F:\workspace\notebook> nvidia-smi
Tue Aug 15 09:23:21 2023
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 528.24       Driver Version: 528.24       CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name            TCC/WDDM | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ... WDDM  | 00000000:01:00.0  On |                  N/A |
| 30%   38C    P8    19W / 350W |    782MiB / 24576MiB |      4%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1620    C+G   C:\Windows\System32\dwm.exe     N/A      |
|    0   N/A  N/A      1908    C+G   ...ge\Application\msedge.exe    N/A      |

安装 Visual Studio 2019 Community

安装 VS2019 [Visual Studio Community 2019](https://my.visualstudio.com/Downloads?q=visual studio 2019&wt.mc_id=o~msft~vscom~older-downloa

验证

安装 Git, CMake, Anaconda

安装 git,

tortoisegit 可以看文件状态

安装 cmake, 跨平台编译时使用;

安装 Anaconda,集成了很多 python 开发环境

验证

下载并安装 OpenCV

OpenCV 下载地址

VC版本号 VS对应版本
vc6 VC6.0
vc7 VS2002
vc7.1 VS2003
vc8 VS2005
vc9 VS2008
vc10 VS2010
vc11 VS2012
vc12 VS2013
vc13 VS2014
vc14 VS2015
vc15 VS2017
vc16 VS2019

既然上面安装的是 VS 2019, 那么我们就安装 VC16 版本的 OpenCV, 省得自己编译了;

解压安装后,将 build 目录下的 x64\vc16\bin 添加到环境变量中。

安装 CUDA 和 CUDNN

这里有些人可能不知道需要安装什么版本的 cuda。因为我这里的 GPU 是 N卡 3090 还是比较好的,所以可以安装比较高阶版本的软件,但是也不能太新,我这里直接参考 PyTorch 里最新版本的框架依赖哪个?

好了,那就安装 CUDA 11.8 和对应的 CUDNN 8 ;

cuda11.8-exe_local-3GB

cudnn 下载对应版本

注意: cudnn 要注册账号

解压后,将 cudnn 文件夹下的所有文件夹复制到 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\ 目录下。

验证

(base) D:\>nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2022 NVIDIA Corporation
Built on Wed_Sep_21_10:41:10_Pacific_Daylight_Time_2022
Cuda compilation tools, release 11.8, V11.8.89
Build cuda_11.8.r11.8/compiler.31833905_0

进入到安装目录 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\extras\demo_suite, 运行 .\deviceQuery.exe

安装 PyTorch

PyTorch

conda 安装

conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia

pip 安装

pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

验证

(base) D:\>python
Python 3.11.4 | packaged by Anaconda, Inc. | (main, Jul  5 2023, 13:38:37) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> print(torch.__version__)
2.0.1
>>> torch.cuda.is_available()
True
>>>

ultralytics/YOLOv8

创建虚拟环境

conda create --name yolov8 --clone base

激活虚拟环境

conda activate yolov8

安装

pip install ultralytics

代码 https://github.com/ultralytics/ultralytics

权重 https://github.com/ultralytics/assets/releases

验证

yolo predict model=yolov8n.pt imgsz=640 conf=0.25
(yolov8) F:\workspace\yolov8>yolo predict model=yolov8n.pt imgsz=640 conf=0.25
WARNING  'source' is missing. Using default 'source=D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\assets'.
Ultralytics YOLOv8.0.154  Python-3.11.4 torch-2.0.1 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)
YOLOv8n summary (fused): 168 layers, 3151904 parameters, 0 gradients

image 1/2 D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\assets\bus.jpg: 640x480 4 persons, 1 bus, 1 stop sign, 160.2ms
image 2/2 D:\anaconda3\envs\yolov8\Lib\site-packages\ultralytics\assets\zidane.jpg: 384x640 2 persons, 1 tie, 154.0ms
Speed: 41.6ms preprocess, 157.1ms inference, 72.6ms postprocess per image at shape (1, 3, 384, 640)
Results saved to runs\detect\predict

【参考】

Windows 安装 CUDA/cuDNN

验证pytorch是否为GPU版本

YOLOv8环境搭建(Windows11)

YOLOv8 从环境搭建到推理训练

Ultralytics YOLOv8 Docs-Quickstart

Anaconda 创建,复制,移植,删除环境

最近一个项目要编译深度学习的库,需要用到 opencv 和 JNI,本文档用于记录环境配置中遇到的常见错误以及解决方案

Invalid Gradle JDK configuration found

failed
Invalid Gradle JDK configuration found
 
Invalid Gradle JDK configuration found. Open Gradle Settings 
Change JDK location

解决办法: 删除文件
.idea/gradle.xml.idea/workspace.xml, 重新编译;

解决办法:Invalid Gradle JDK configuration found

clang++: error: unknown argument: ‘-static-openmp’

原因是NDK版本过高,跟当前的AndroidStudio版本不匹配。选择升级AndroidStudio或者降低NDK版本即可。

重新下载21.3.6528147版本,配置NDK通过,rebuild项目通过,问题解决, 最新版的 Android Studio 降级到 Android Studio 4.2.1 版本,NDK 降级到 21.3.6528147;

问题:clang++.exe: error: unknown argument: ‘-static-openmp‘

NDK版本!clang++: error: unknown argument: ‘-static-openmp‘

Android res\values-v26\values-v26.xml:9:5-12:13: AAPT: error: resource android:attr/colorError not f

解决方法:
compileSdkVersion 设置为28

Android res\values-v26\values-v26.xml:9:5-12:13: AAPT: error: resource android:attr/colorError not f

2 files found with path ‘lib/arm64-v8a/xxx.so‘ 问题

最后在自己的 nativelib modulebuild.gradleandroid{} 加上

sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

2 files found with path ‘lib/arm64-v8a/xxx.so‘ 问题

undefined reference to `cv::String::deallocate()一种可能解决方案

解决方案:替换库文件时候,同步替换头文件

undefined reference to `cv::String::deallocate()一种可能解决方案

关于解决gradle版本与gradle插件版本不一致问题的方法之一

关于解决gradle版本与gradle插件版本不一致问题的方法之一

Algorithm HmacPBESHA256 not available

D:\Android\sdk\.android 的目录下找到文件 debug.keystore, 其实不缺少签名文件,这个问题,应该是之前安装了多个版本的 Android Studio,导致签名文件被覆盖了, 删除 debug.keystore 文件,重新签名即可。

JNI GetFieldID和GetMethodID函数解释及方法签名

  1. GetFieldID 是得到 java 类中的参数 IDGetMethodID 得到 java 类中方法的 ID ,它们只能调用类中声明为 public 的参数或方法。

举例说明:

jclass c = (*env)->FindClass(env,"com/camera/webcam/Test");
jfieldID width_id = (*env)->GetFieldID(env, c, "width", "I");

第一个参数:JNI接口对象;第二个参数:Java类对象;第三个参数:参数名(或方法名);第四个参数:该参数(或方法)的签名。

  1. 方法签名

调用 JNIGetMethodID 函数获取一个 jmethodID 时,需要传入一个方法名称和方法签名,方法名称就是在 Java 中定义的方法名,方法签名的格式为:(形参参数类型列表)返回值。

JNI GetFieldID和GetMethodID函数解释及方法签名

如何从JNI返回多个数组到Java?

当您希望从一个函数返回多个“东西”时,您有两个选项(这并不是 JNI 特有的):要么创建一个包含所有结果的包装器对象(在您的例子中,是一个包含3个数组字段的 Java 类),要么使用 out 参数。也许,在您的情况下,如果您知道调用前的长度,后者可能会更容易一些。

所以,在 Java 中,您可以编写如下内容

package p;

public class C {
    public void f() {
        byte[] array1 = new byte[10];
        int[] array2 = new int[20];
        String[] array3 = new String[5];
        fillArrays(array1, array2, array3);
    }
    native void fillArrays(byte[] byteArray, int[] intArray, String[] stringArray);
}

现在,在 C 中,这看起来是这样的:

JNIEXPORT void JNICALL 
Java_p_C_fillArrays(JNIEnv *env, jobject thisC, jbyteArray byteArray, jintArray intArray, jobjectArray stringArray)
{
    jboolean isCopy;
    jint i = 0;
    char* names[] = {"one", "two", "three"};

    jbyte *c_byteArray = (*env)->GetByteArrayElements(env, byteArray, &isCopy);
    for (i=0; i<(*env)->GetArrayLength(env, byteArray); i++) {
        c_byteArray[i] = (jbyte)i;
    }
    (*env)->ReleaseByteArrayElements(env, byteArray, c_byteArray, 0);

    jint *c_intArray = (*env)->GetIntArrayElements(env, intArray, &isCopy);
    for (i=0; i<(*env)->GetArrayLength(env, intArray); i++) {
        c_intArray[i] = i;
    }
    (*env)->ReleaseIntArrayElements(env, intArray, c_intArray, 0);

    for (i=0; i<(*env)->GetArrayLength(env, stringArray) && i<sizeof(names)/sizeof(names[0]); i++) {
        (*env)->SetObjectArrayElement(env, stringArray, i, (*env)->NewStringUTF(env, names[i]));
    }
}

如何从JNI返回多个数组到Java?

Java: JNI对数组赋值并返回给Java

JNI 中对 Java 层的数组赋值有两种方式:一是在 Java 层创建好数组,然后传递到 JNI 层,由 JNI 层进行赋值;二是直接在 JNI 层创建好数组并赋值,然后返回数组到 Java 层。下面是两种方式的对比实现:

创建两个 native 方法

    //传递数组,操作后,返回
    public native void passArrayMethod(int[] arr);

    //创建指定长度数组
    public native int[] createArrayMethod(int len);

生成对应的 C 函数

JNIEXPORT void JNICALL Java_com_test_git_jnidemo_JniUtil_JniDemo_passArrayMethod
  (JNIEnv *, jobject, jintArray);

JNIEXPORT jintArray JNICALL Java_com_test_git_jnidemo_JniUtil_JniDemo_createArrayMethod
  (JNIEnv *, jobject, jint);

传递数组给 JNI ,修改第一个元素值,然后排序

int com(const void *a, const void *b){
    return *(int *)a - *(int *)b;//升序
}
JNIEXPORT void JNICALL Java_com_test_git_jnidemo_JniUtil_JniDemo_passArrayMethod
        (JNIEnv *env, jobject jobj, jintArray jarr){
    //1.获取数组指针
    jint *arr = env->GetIntArrayElements(jarr, NULL);
    *arr = 100;
    //2.获取数组长度
    int len = env->GetArrayLength(jarr);
    //3.排序
    qsort(arr, len, sizeof(jint), com);

    //4.释放资源
    env->ReleaseIntArrayElements(jarr, arr, JNI_COMMIT);
//    env->ReleaseIntArrayElements(jarr, arr, JNI_ABORT);
    //  对于最后一个参数(如果指针指向的数组为副本时,否则该参数不起作用)
    //      0       copy back the content and free the elems buffer
    //      JNI_COMMIT      copy back the content but do not free the elems buffer
    //      JNI_ABORT       free the buffer without copying back the possible changes
};

JNI 生成数组,并返回

JNIEXPORT jintArray JNICALL Java_com_test_git_jnidemo_JniUtil_JniDemo_createArrayMethod
        (JNIEnv *env, jobject jobj, jint len){
    //1.新建长度len数组
    jintArray jarr = env->NewIntArray(len);
    //2.获取数组指针
    jint *arr = env->GetIntArrayElements(jarr, NULL);
    //3.赋值
    int i = 0;
    for(; i < len; i++){
        arr[i] = i;
    }
    //4.释放资源
    env->ReleaseIntArrayElements(jarr, arr, 0);
    //5.返回数组
    return jarr;
};

MainActivity 中调用

        int[] arr = {1, 3, 2, 6, 8, 0};
        Log.i(TAG, "arr修改前: " + getArrayString(arr));
        jd.passArrayMethod(arr);
        Log.i(TAG, "arr修改后: " + getArrayString(arr));

        Log.i(TAG, "------------------------------------------");

        int[] arr_new = jd.createArrayMethod(10);
        Log.i(TAG, "arr_new: "+ getArrayString(arr_new) );

输出结果:

09-26 17:02:29.454 994-994/com.test.git.jnidemo I/MainActivity-: arr修改前: ,1,3,2,6,8,0
09-26 17:02:29.454 994-994/com.test.git.jnidemo I/MainActivity-: arr修改后: ,0,2,3,6,8,100
09-26 17:02:29.454 994-994/com.test.git.jnidemo I/MainActivity-: ------------------------------------------
09-26 17:02:29.454 994-994/com.test.git.jnidemo I/MainActivity-: arr_new: ,0,1,2,3,4,5,6,7,8,9

Java: JNI对数组赋值并返回给Java

[JNI 编程上手指南之 JNIEnv 详解]

  1. JNIEnv 是什么?

JNIEnvJava Native Interface EnvironmentJava 本地编程接口环境。JNIEnv 内部定义了很多函数用于简化我们的 JNI 编程。
JNIJava 中的所有对象或者对象数组当作一个 C 指针传递到本地方法中,这个指针指向 JVM 中的内部数据结构(对象用 jobject 来表示,而对象数组用 jobjectArray 或者具体是基本类型数组),而内部的数据结构在内存中的存储方式是不可见的。只能从 JNIEnv 指针指向的函数表中选择合适的 JNI 函数来操作 JVM 中的数据结构。

C 语言中,JNIEnv 是一个指向 JNINativeInterface_ 结构体的指针:

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv; // C 语言
#endif

struct JNINativeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);

    jclass (JNICALL *DefineClass)
      (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
       jsize len);

    jstring (JNICALL *NewStringUTF)
      (JNIEnv *env, const char *utf);

    //省略其他函数指针
    //......
}

JNINativeInterface_ 结构体中定义了非常多的函数指针,这些函数用于简化我们的 JNI 编程。C 语言中,JNIEnv 中函数的使用方式如下:

JNIEnv * env
// env 的实际类型是 JNINativeInterface_**
(*env)->NewStringUTF(env,"Hello from JNI !");

C++ 代码中,JNIEnv 是一个 JNIEnv_ 结构体:

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv; 
#endif

struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus

    jint GetVersion() {
        return functions->GetVersion(this);
    }
    jclass DefineClass(const char *name, jobject loader, const jbyte *buf,
                       jsize len) {
        return functions->DefineClass(this, name, loader, buf, len);
    }
    jclass FindClass(const char *name) {
        return functions->FindClass(this, name);
    }
    jmethodID FromReflectedMethod(jobject method) {
        return functions->FromReflectedMethod(this,method);
    }
    jfieldID FromReflectedField(jobject field) {
        return functions->FromReflectedField(this,field);
    }

    jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic) {
        return functions->ToReflectedMethod(this, cls, methodID, isStatic);
    }

    jclass GetSuperclass(jclass sub) {
        return functions->GetSuperclass(this, sub);
    }
    //省略其他函数
    //......
}

JNIEnv_ 结构体中同样定义了非常多的成员函数,这些函数用于简化我们的 JNI 编程。C++ 语言中,JNIEnv 中函数的使用方式如下:

//JNIEnv * env
// env 的实际类型是 JNIEnv_*
env->NewstringUTF ( "Hello from JNI ! ");

更多详情见: JNI 编程上手指南之 JNIEnv 详解

Android NDK开发:JNI实战篇

java调用本地方法–JNI访问List集合

JNI系列(四)JAVA数据类型和JNI类型对照表

【参考】

Android Studio配置OpenCV的JNI接口

【Android+OpenCV】Android Studio的安装全过程+在Android Studio中配置OpenCV-重点关注

NDK开发遇到的三个错误:‘javah’ 不是内部或外部命令,编码GBK的不可映射字符, 程序包XX.XX不存在

OpenCV 在 Android Studio 的使用教程

Android Studio使用OpenCV进行图像基本处理

Android学习笔记之——基于Android的opencv开发(Android studio3.6+opencv4.3.0开发环境搭建)

解决办法:Invalid Gradle JDK configuration found

NDK版本!clang++: error: unknown argument: ‘-static-openmp‘

问题:clang++.exe: error: unknown argument: ‘-static-openmp‘

Android res\values-v26\values-v26.xml:9:5-12:13: AAPT: error: resource android:attr/colorError not f

JNI GetFieldID和GetMethodID函数解释及方法签名

如何从JNI返回多个数组到Java?

Java: JNI对数组赋值并返回给Java

JNI 编程上手指南之 JNIEnv 详解

Android NDK开发:JNI实战篇

java调用本地方法–JNI访问List集合

JNI系列(四)JAVA数据类型和JNI类型对照表

在工作中常常需要将图像转化为raw数据或者yuv数据,这里将提供 cpp 版本和 python 版本的互转代码

代码链接见文档尾部。

cpp 版本

jpg2raw.cpp

#include <fstream>
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

// Windows 和 Linux 下通用的创建目录函数
#ifdef _WIN32
#include <direct.h> // For _mkdir on Windows
#define MKDIR(dir) _mkdir(dir)
#else
#include <sys/stat.h> // For mkdir on POSIX systems
#define MKDIR(dir) mkdir(dir, 0755)
#endif

bool create_directory(const std::string &dir_path)
{
#ifdef _WIN32
    if (_mkdir(dir_path.c_str()) != 0)
    {
        return false;
    }
#else
    if (MKDIR(dir_path.c_str()) != 0 && errno != EEXIST)
    {
        return false;
    }
#endif
    return true;
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        std::cerr << "Usage: ./image_to_raw input_image.jpg" << std::endl;
        return -1;
    }

    std::string input_image_path = argv[1];
    std::string base_name = input_image_path.substr(input_image_path.find_last_of('/') + 1); // Unix风格路径分隔符
#ifdef _WIN32
    std::replace(base_name.begin(), base_name.end(), '\\', '/'); // 对于Windows下的反斜杠,统一转换为正斜杠
#endif

    std::cout <<" ----------- input_image_path: " << input_image_path << " ------------------------"<<std::endl;
    std::cout <<" ----------- basename: " << base_name << " ------------------------"<<std::endl;

    std::string raw_dir = "raws/";
    // std::string output_raw_path = raw_dir + base_name + ".raw";

    std::string file_name = input_image_path.substr(input_image_path.find_last_of("/\\") + 1);
    std::string file_name_without_suffix = file_name.substr(0, file_name.find_last_of("."));
    std::string file_name_with_suffix = file_name_without_suffix + ".raw";
    std::string output_raw_path = raw_dir + file_name_with_suffix;


    // 创建 'raw' 子目录(如果不存在)
    if (!create_directory(raw_dir))
    {
        std::cerr << "Failed to create directory: " << raw_dir << std::endl;
        //return -1;
    }

    cv::Mat img = cv::imread(input_image_path, 1);
    if (img.empty())
    {
        std::cerr << "Failed to load the image: " << input_image_path << std::endl;
        return -1;
    }

    int im_width = img.cols;
    int im_height = img.rows;
    int im_depth = img.channels();

    std::cout << "im_width: " << im_width << std::endl;
    std::cout << "im_height: " << im_height << std::endl;
    std::cout << "im_depth: " << im_depth << std::endl;

    // 写入 RAW 文件
    std::ofstream raw_file(output_raw_path, std::ios::binary);
    if (!raw_file.is_open())
    {
        std::cerr << "Failed to open file for writing: " << output_raw_path << std::endl;
        return -1;
    }
    raw_file.write(reinterpret_cast<const char *>(img.data), im_width * im_height * im_depth);
    raw_file.close();

    std::cout << "Converted " << input_image_path << " to " << output_raw_path << std::endl;

    return 0;
}

raw2jpg

#include <fstream>
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <unordered_map>

#ifdef _WIN32
#include <direct.h> // For _mkdir on Windows
#define MKDIR(dir) _mkdir(dir)
#else
#include <sys/stat.h> // For mkdir on POSIX systems
#define MKDIR(dir) mkdir(dir, 0755)
#endif

struct ImageSize
{
    int width;
    int height;
};

const std::unordered_map<int, ImageSize> image_sizes = {
    {921600, {640, 480}},
    {2764800, {1280, 720}},
    {5858640, {1896, 1030}},
    {6220800, {1920, 1080}},
    {11059200, {2560, 1440}},
    {12257280, {2688, 1520}},
    {15116544, {2592, 1944}},
    {3686400, {1280, 960}}};

// 跨平台创建目录函数(仅支持Linux和Windows)
bool create_directory(const std::string &path)
{
#ifdef _WIN32
    return (_mkdir(path.c_str()) == 0);
#else
    return (::mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0);
#endif
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        std::cerr << "Usage: ./raw_image_viewer raw_image.raw" << std::endl;
        return -1;
    }

    std::string raw_image_path = argv[1];
    std::cout << "raw_image_path: " << raw_image_path << std::endl;

    // 获取文件大小
    std::ifstream file(raw_image_path, std::ios::binary | std::ios::ate);
    size_t file_size = file.tellg();
    file.close();
    std::cout << "size: " << file_size << " bytes" << std::endl;

    // 查找对应的图像尺寸
    auto it = image_sizes.find(file_size);
    if (it == image_sizes.end())
    {
        std::cout << file_size << " is not supported!" << std::endl;
        return -1;
    }
    const ImageSize &size = it->second;
    int im_width = size.width;
    int im_height = size.height;
    int im_depth = 3; // 假设所有图像都是RGB三通道

    std::cout << "im_width: " << im_width << std::endl;
    std::cout << "im_height: " << im_height << std::endl;
    std::cout << "im_depth: " << im_depth << std::endl;

    cv::Mat raw_image(im_height, im_width, CV_8UC3);

    // 读取 RAW 文件的内容
    std::ifstream raw_file(raw_image_path, std::ios::binary);
    raw_file.read(reinterpret_cast<char *>(raw_image.data), im_width * im_height * im_depth);
    raw_file.close();

    // 提取并创建 images 文件夹(跨平台)
    std::string images_path = "images/";
    bool dir_created = create_directory(images_path);

    std::string file_name = raw_image_path.substr(raw_image_path.find_last_of("/\\") + 1);
    std::string file_name_without_suffix = file_name.substr(0, file_name.find_last_of("."));
    std::string file_name_with_suffix = file_name_without_suffix + ".jpg";
    std::string output_path = images_path + file_name_with_suffix;

    cv::imwrite(output_path, raw_image);

    std::cout << " images_path: " << images_path << std::endl;
    std::cout << " file_name_with_suffix: " << file_name_with_suffix << std::endl;
    std::cout << " file save name: " << output_path << std::endl;

    if (!dir_created)
    {
        std::cerr << "Failed to create 'images' directory." << std::endl;
    }

    // (可选)显示图像
    // cv::imshow("raw_image", raw_image);
    // cv::waitKey(5);
    // cv::destroyAllWindows();

    // (可选)删除原始 RAW 文件
    // std::remove(raw_image_path.c_str());

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(convertRAW2jpg CXX)

# 设置安装目录
set(CMAKE_INSTALL_PREFIX "${PROJECT_BINARY_DIR}/install")
set(CMAKE_SKIP_INSTALL_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")

# 要求编译器支持C++11
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Windows 特定选项和标志
if(MSVC)
    add_compile_options(/utf-8)
    add_definitions(-D_CRT_SECURE_NO_WARNINGS)
endif()


# OpenCV 手动配置部分(保留 find_package 并注释)
if(WIN32)
    # find_package(OpenCV REQUIRED)  # 注释掉以使用自定义路径
    set(OpenCV_DIR "D:/opencv480vc16/build" CACHE PATH "Path to a custom OpenCV build on Windows")
    set(OPENCV_INCLUDE_DIRS "${OpenCV_DIR}/include")
    set(OPENCV_LIB_DIR     "${OpenCV_DIR}/x64/vc16/lib") # 根据实际编译器版本调整

    include_directories(${OPENCV_INCLUDE_DIRS})
    link_directories(${OPENCV_LIB_DIR})

    list(APPEND EXTRA_LIBS opencv_world480.lib) # 根据实际OpenCV库名称调整
elseif(NOT WIN32)
    # Linux 下的手动 OpenCV 配置
    # find_package(OpenCV REQUIRED)  # 注释掉以使用自定义路径
    set(OpenCV_DIR "~/install/opencv4.5.4" CACHE PATH "Path to a custom OpenCV build on Unix-like systems")
    set(OPENCV_INCLUDE_DIRS "${OpenCV_DIR}/include/opencv4")
    set(OPENCV_LIB_DIR     "${OpenCV_DIR}/lib")

    include_directories(${OPENCV_INCLUDE_DIRS})
    link_directories(${OPENCV_LIB_DIR})

    list(APPEND EXTRA_LIBS opencv_world -fopenmp -lpthread -lm) # 根据实际OpenCV库名称和依赖调整
endif()

include_directories(${OpenCV_INCLUDE_DIRS})

# 添加源文件
add_executable(raw2jpg src/raw2jpg.cpp)
add_executable(jpg2raw src/jpg2raw.cpp)


target_link_libraries(raw2jpg PRIVATE ${EXTRA_LIBS})
target_link_libraries(jpg2raw PRIVATE ${EXTRA_LIBS})

# 安装目标及辅助脚本
set(TMP_TEST_DIR ${PROJECT_SOURCE_DIR}/temp_example)
install(TARGETS raw2jpg
        RUNTIME DESTINATION examples
        RUNTIME DESTINATION ${TMP_TEST_DIR})

install(TARGETS jpg2raw
        RUNTIME DESTINATION examples
        RUNTIME DESTINATION ${TMP_TEST_DIR})

# 找到 assets 目录下所有的 raw 文件,并安装到临时目录
FILE(GLOB RAWS assets/raws/*.raw)
FILE(GLOB JPGS assets/jpgs/*.jpg)

install(FILES ${RAWS} DESTINATION ${TMP_TEST_DIR}/test_raws)
install(FILES ${JPGS} DESTINATION ${TMP_TEST_DIR}/test_jpgs)

if (WIN32)
    install(FILES ${PROJECT_SOURCE_DIR}/onekey_windows_batch2jpg.bat
            DESTINATION examples
            DESTINATION ${TMP_TEST_DIR})
    install(FILES ${PROJECT_SOURCE_DIR}/onekey_windows_batch2raw.bat
            DESTINATION examples
            DESTINATION ${TMP_TEST_DIR})
else()
    install(FILES ${PROJECT_SOURCE_DIR}/onekey_ubuntu_batch2jpg.sh
            DESTINATION examples
            DESTINATION ${TMP_TEST_DIR})
    install(FILES ${PROJECT_SOURCE_DIR}/onekey_ubuntu_batch2raw.sh
            DESTINATION examples
            DESTINATION ${TMP_TEST_DIR})
endif(WIN32)

批量将 raw 转换为 jpg 的脚本

ubuntu

onekey_ubuntu_batch2jpg.sh

#!/bin/bash
set -e

if [ $# -eq 0 ]; then
	im_path=`pwd`
else
	im_path=$1
fi

echo "batch convert the raw image to jpg images in path: $im_path"
i=0
for im in $(ls $im_path)
do 
	if [ "${im##*.}" = "raw" ]; then
		if [ -f $im_path/$im ];then
			echo "convert $file to jpg image..."
            ./raw2jpg $im_path/$im
			let i+=1
		fi
	fi
done 
echo "DONE: $i pictures been gennerated!"**s**

windows

onekey_windows_batch2jpg.bat

@echo off

@REM 如果没有输入路径, 就使用当前路径
@if "%~1"=="" (
    set "im_path=%cd%"
) else (
    set "im_path=%~1"
)

@REM 输出批处理将要转换文件的路径信息
echo Batch converting RAW images to JPG in the current directory or specified path: %im_path%

@REM 遍历指定目录及其子目录下的所有.raw文件
for /R "%im_path%" %%i in (*.raw) do (
    @REM 获取图片名称(不包括完整路径)
    set "img_file=%%~ni"
    @REM 输出正在处理的原始图像文件名
    echo Processing image: %%i
    @REM 调用 raw2jpg.exe 进行转换,由于程序内部已经设置了输出目录和文件名规则,所以这里仅传入原始文件路径即可
    .\raw2jpg.exe "%%i"
)

@REM 暂停脚本执行,等待用户按键继续
pause

批量将 jpg 转换为 raw 的脚本

ubuntu

onekey_ubuntu_batch2raw.sh

#!/bin/bash
set -e

if [ $# -eq 0 ]; then
	im_path=`pwd`
else
	im_path=$1
fi

echo "batch convert the jpg images to raw image in path: $im_path"
i=0
for im in $(ls $im_path)
do 
	if [ "${im##*.}" = "jpg" ]; then
		if [ -f $im_path/$im ];then
			echo "convert $file to jpg image..."
            ./jpg2raw $im_path/$im
			let i+=1
		fi
	fi
done 
echo "DONE: $i pictures been gennerated!"s

windows

onekey_windows_batch2raw.bat

@echo off

@REM 如果没有输入路径, 就使用当前路径
if "%1"=="" (
    set "im_path=%cd%"
) else (
    set "im_path=%~1"
)

@REM 输出当前处理的路径
echo Batch converting image files to raw images in path: %im_path%

@REM 遍历指定目录及其子目录下的所有.jpg, .jpeg, .bmp, .png文件
for /R "%im_path%" %%i in (*.jpg, *.jpeg, *.bmp, *.png) do (
    @REM 获取图片名称(这里保留完整路径)
    echo Processing image: %%i
    .\jpg2raw.exe "%%i"
)

pause

所有代码见

raw2jpg-github

问题

Could not load the Qt platform plugin "xcb" in "/home/<username>/miniconda3/envs/test/lib/python3.8/site-packages/cv2/qt/plugins" even though it was found.
This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.

Available platform plugins are: xcb, eglfs, linuxfb, minimal, minimalegl, offscreen, vnc, wayland-egl, wayland, wayland-xcomposite-egl, wayland-xcomposite-glx, webgl.

解决方案

pip3 install opencv-python-headless

参考引用

Solutions to possible problems

0%